What I Learned About API Design While Working on a Pydantic Logfire PR
I was working on a PR for Pydantic's Logfire project when I stumbled into something that completely changed how I think about API design. What started as a simple contribution led me down a rabbit hole that taught me how library maintainers actually make decisions in the real world.
How I Got Here
While digging through the Logfire codebase, I noticed some patterns that seemed connected to OpenTelemetry. Following that thread, I ended up looking at aiohttp's source code and found this interesting discussion about inheritance vs composition.
The conversation was about discouraging users from inheriting from web.Application
- aiohttp's main class. At first, this seemed backward to me. Isn't inheritance a fundamental part of object-oriented programming?
The Problem
Users naturally want to extend aiohttp's Application
class like this:
class MyApp(web.Application): def my_custom_method(self): return "something useful"
But the aiohttp maintainers strongly discourage this. Instead, they want you to use composition:
class MyApp: def __init__(self): self.app = web.Application() self.app['my_app'] = self def my_custom_method(self): return "something useful"
Why This Matters
Here's what I learned about the real reasons behind this design choice:
The Collision Problem: Imagine you create a method called foo()
in your inherited class. Everything works great. Then, six months later, aiohttp releases a new version that adds a foo
attribute to the base Application
class. Your code breaks in weird, hard-to-debug ways.
Namespace Pollution: When you inherit, you're mixing your stuff with the framework's stuff. The maintainer put it perfectly: "it adds user names for base application, application attributes from future versions of aiohttp may clash with user ones."
Not Built for It: web.Application
wasn't designed as a base class. The framework uses signals and callbacks instead of method overriding patterns.
The Trade-offs
The composition approach isn't perfect. You end up with circular references:
-
my_app.app
points to the web application -
app['my_app']
points back to your class
Type checkers also struggle with app['my_app']
- they don't know what type it is, while inheritance would give you proper typing.
One user in the discussion pointed out: "don't you find it to be a not-so-good OOP design?" They had a point.
What I Realized
This conversation showed me that API design isn't about textbook principles. It's about making hard choices between competing priorities:
- Stability vs Convenience
- Type Safety vs Flexibility
- Clean Code vs Future-Proof Code
The aiohttp maintainers chose stability over convenience. They'd rather have users write slightly more verbose code than risk breaking existing applications when they add new features.
The Real World is Messy
Before this, I thought good API design meant following OOP principles and making things easy for users. But maintainers have to think about problems I never considered:
- What happens when we add features in version 2.0?
- How do we avoid breaking thousands of applications?
- What if users depend on implementation details we want to change?
The maintainer even acknowledged: "I understand that the solution is not ideal. Maybe we need to come up with a better solution but it should be not an inheritance."
What This Taught Me
Working on that Logfire PR and following this thread taught me that good API design is about predicting the future and making conservative choices. Sometimes the "wrong" solution today prevents bigger problems tomorrow.
It also made me appreciate the thought that goes into the libraries we use every day. These decisions aren't made lightly - they come from experience dealing with real users and real problems.
Next time I'm frustrated with a library's design choices, I'll remember this conversation. There's probably a good reason for what seems like an arbitrary decision.
Have you encountered similar API design decisions that surprised you? I'd love to hear about them in the comments.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.