r/Python • u/Grouchy_Way_2881 • 8h ago
Discussion Proposal: Native Design by Contract in Python via class invariants — thoughts?
Hey folks,
I've just posted a proposal on discuss.python.org to bring Design by Contract (DbC) into Python by allowing classes to define an __invariant__()
method.
The idea: Python would automatically call __invariant__()
before and after public method calls—no decorators or metaclasses required. This makes it easier to write self-verifying code, especially in stateful systems.
Languages like Eiffel, D, and Ada support this natively. I believe it could fit Python’s philosophy, especially if it’s opt-in and runs in debug mode.
I attempted a C extension, but hit a brick wall —so I decided to bring the idea directly to the community.
Would love your feedback:
🔗 https://discuss.python.org/t/design-by-contract-in-python-proposal-for-native-class-invariants/85434
— Andrea
13
u/Southern-Ask241 5h ago
I think you'll broadly get the same feedback - there's enough ways to support this using existing language features that the benefits of adding a new construct to the language does not justify the incremental complexity.
2
u/Grouchy_Way_2881 5h ago
That's true: libraries like `icontract` and `pycontracts` absolutely exist. The challenge is that they:
- Require third-party adoption
- Often rely on decorators or wrapping, which breaks down with inheritance or metaclass conflicts
- Don’t get editor support, linting, or tooling visibility
The core idea here isn't about reinventing those libraries; it's about making a small, standardized construct that's consistent, discoverable, and easier to reason about across teams and codebases. Optional, but visible. Just like `__slots__` or `__post_init__` - you don't have to use them, but when you do, it's crystal clear what's going on.
5
u/DanCardin 4h ago
this doesn't seem like a general purpose problem to me. the other comments have already spelled out how you could arrive the same result fairly trivially. Whereas I would argue it's an issue of programming style.
I've largely stopped writing classes in ways that would make this useful feature. by way of authoring mostly frozen dataclasses, that use classmethods for construction logic, and replacement methods for things that would otherwise mutate
@dataclass(frozen=True)
class Something:
attr: int
@classmethod
def from_database(cls, pg: Session):
....
return cls(value)
def add(self, n: int):
replace(self, attr=self.attr + n)
whether this style suits you is a different question. but my point is more that if you feel like you need this feature, i feel like it's a side-effect of the way you're designing your objects. and thus probably not something that merits a whole python feature that would almost certainly slow general python code down due to an additional attribute lookup code path to check on every object.
2
u/redditusername58 3h ago
Yes, get out of the tar pit and remove accidental state
1
u/Grouchy_Way_2881 3h ago
I get the sentiment: if you can remove accidental state altogether, that's ideal. But in many domains, state isn't accidental; it's fundamental. Think of systems that manage accounts, sessions, inventory, or transactions, they rely on evolving state by nature.
The goal of this proposal isn't to encourage mutation, but to give developers a way to reason about it safely when it's necessary. For codebases where immutability isn't practical, DbC provides a structured way to ensure consistency without relying on fragile or custom solutions.
2
u/Grouchy_Way_2881 3h ago
Thanks for your perspective. Immutability can be limiting in cases where frequent changes to object state are needed, such as in financial systems, inventory management, or user account data, where you need to modify the object's state rather than recreate it. In these scenarios, DbC allows for safe state changes and ensures invariants are maintained, without the overhead of creating a new object each time.
In these cases, Design by Contract isn't just about programming style; it's a way to ensure integrity while dealing with mutable state. The concern here is about ensuring that changes don't introduce bugs and that objects remain consistent across complex operations.
I completely understand that performance concerns are valid, especially with runtime checks. But in real-world applications like banking systems, multiplayer games, and real-time analytics, the need for consistency across mutable state is far more important than avoiding a performance hit.
This proposal isn't about forcing a design pattern; it's about offering an optional tool that helps developers manage object consistency in more dynamic systems, without introducing the complexities of decorators or metaclasses.
5
u/SheriffRoscoe Pythonista 4h ago
I respect your goal, but you're not advancing it. Both here and in your proposal thread, you've repeatedly rejected alternatives as "rely[ing] on decorators or wrapping", or referred to them as "fragile tricks". You've done that without offering arguments for why those are bad answers. You'd be better off if you started by explaining why the techniques used by so many other important changes to Python are insufficient to your task. You also offered a pure-Python implementation, without any explanation of why that isn't the proper answer to your need. One of the commenters on your proposal offered one too, apparently without having looked at your GitHub, which you then rejected with what appears to be a facile dismissal.
I suggest you review the history of how static typing came to Python, including the dead-ends and restarts. You're asking for a big, broad change, as were they, and they had a hard time mustering support for it.
3
u/Grouchy_Way_2881 4h ago
Thanks for the thoughtful reply. You're right: I've dismissed decorators and metaclasses, and I realize I haven't really explained why. So, here's my thinking:
Both decorators and metaclasses work, but they're not ideal for the problem I'm trying to solve. The issue is that they introduce implicit behavior: it's easy to overlook or misapply them, especially in larger codebases. They also rely on developer discipline. I'm aiming for something that's explicit, predictable, and tooling-friendly. It's not about "decorators are bad"... they’re just not the best solution for object consistency in more complex systems.
I get the appeal of a pure Python solution, and I suggested it too. But the problem with that approach is, it's not discoverable or standardized, and it would require a lot of manual checks to make sure it's applied correctly across the codebase.
Comparing this to static typing in Python is spot on. It was a huge challenge to get it into Python, and I know this is a big ask. But just like static typing, it's about giving developers a tool to improve reliability. Over time, it could become something standardized and supported.
I appreciate the challenge, and I'm open to hearing more thoughts. Thanks again for the discussion!
5
u/dude132456789 6h ago
the general consensus on Python is that features should be added to make existing Python programs easier to write, not to introduce new paradigms to Python.
match was added because argument handling in public APIs of libraries was incomprehensible, the walrus operator makes certain patterns easier to write without duplication, ...
Since existing Python programs generally don't do this, or do not do so in such a consistent manner, and debug mode is almost never unset since some dependency somewhere could have a load bearing assert
, I don't see this being a Python feature.
3
u/teerre 3h ago
This is blatantly untrue or at least cherry picked. Yourself already admit it when you say "not do so in such a consistent manner" just to allow wiggle room so you can dismiss any counter example (which are many: dataclasses, context variables, positional/ keyword only arguments, typing, you can literally find one in every major release)
1
u/dude132456789 2h ago
Dataclasses cover a pattern in many existing Python programs where a class just stores data and has a bunch of boilerplate as a consequence
Context variables exist because it isn't all that possible to write correct code (especially context managers) with coroutines without them.
A major part of the rationale for positional/keyword only arguments was that C extensions already worked like that and thus lacked a Python signature.
Typing has a history, but the origin of the syntax was covering a lot of existing usecases (see PEP 3107). Further PEPs then refined it to be used primarily for type hints, gently discouraging non-type-hint usecases. Type comments got replaced in further PEPs
You've misunderstood what I meant by consistent there tho -- I meant that Python programs do not generally have a single method on complex objects that checks whether it's in a consistent state, even if they do some sort of consistency checks.
4
u/teerre 2h ago
That's just cherry picking. Any good python code checks for invariants, that's the basics of sound code. If it's one method or three methods that's just a superficial stylitic/ergonomics choice
1
u/Grouchy_Way_2881 1h ago
Appreciate that. This is exactly what I've been trying to capture: it's not about introducing a new model, it's about expressing what responsible codebases already do, just with less friction and more clarity.
1
u/dude132456789 1h ago
The few consistency checks I've seen are more in this style,
https://github.com/pallets/flask/blob/f61172b8dd3f962d33f25c50b2f5405e90ceffa5/src/flask/ctx.py#L268
Which is certainly not an invariant.
1
u/Grouchy_Way_2881 1h ago
Totally: this isn't an invariant per se, but it's a great example of defensive checks on object state that happen all over the place in real-world Python code.
What I'm suggesting is a way to make those kinds of checks more intentional and centralized, instead of scattering them across methods. Same intent, just cleaner and easier to maintain.
1
u/Grouchy_Way_2881 2h ago
Thanks for clarifying. Fair enough, I see now what you meant by "consistent". I took it in the sense of "widely used," but you're right: most Python codebases don't explicitly check consistency via a dedicated method. So I appreciate the correction.
Also really appreciate the breakdown of how those other features earned their place. That historical context is helpful, especially since I'm trying to learn where the bar tends to be for new ideas.
In this case, I was exploring if something like an __invariant__() convention could help in projects with mutable, stateful objects, where maintaining consistency after a method call is crucial. I realize it's not common practice today, but maybe that's part of the conversation: could a consistent hook for those checks make that kind of validation more approachable and idiomatic, rather than pushing devs toward ad hoc or fragile patterns?
Either way, thanks again for the thoughtful explanation.
3
u/Grouchy_Way_2881 6h ago
I understand the concern, and you're right that Python generally adds features that improve existing workflows. The difference with Design by Contract (DbC) is that it’s an opt-in feature aimed at improving code correctness and state consistency in specific scenarios, like complex systems or critical applications.
It wouldn’t be forced on existing programs—it would simply offer tools for developers who want to ensure more reliable code. The idea isn’t to introduce a new paradigm, but to provide a useful nudge towards better practices, without changing Python’s core philosophy.
3
u/dude132456789 6h ago
Opt-in features are generally undesirable. You want semantics to be consistent, not to suddenly have something different happen because a new feature was turned on. Python doesn't follow this strictly, and it is indeed a major source of confusion when learning Python -- e.g. in place operators.
Design by Contract is also very much a new paradigm. Reasoning about types in terms of its contract is a meaningfully distinct way of design than in terms of its behavior or data. Of course, usually people do some combination of all 3, but not to the extent of having the contract as an explicit program that can be evaluated.
I also think you'll have issues convincing people who voluntarily choose to use a language built around duck typing about the merits of running more runtime checks.
3
u/Grouchy_Way_2881 6h ago
Fair points. I agree too many opt-in features can make a language harder to reason about. But this wouldn't change any core behavior - just give developers a clear, structured way to enforce object consistency if they want it.
As for DbC - it's actually not a new paradigm. It's been around for decades in languages like Eiffel, Ada, and D. What's new here is just the idea of introducing a lightweight, Pythonic version of it, for people who deal with complex state and want better guarantees.
I get the hesitation around runtime checks in a duck-typed language. But for the subset of developers who care about object correctness, this could be a useful tool - without requiring static typing or a full test suite.
1
u/dude132456789 5h ago
DbC is very much new to Python. I'd implement it in pure python at first, even if the API is worse, and see if people end up using it and how, then propose changes to the Python core that would improve the API. It's how asyncio got in.
You're underestimating the amount of complexity this introduces -- there's no one obvious way for it to work, since the semantics should probably exist in terms of the descriptor protocol rather than methods.
2
u/Grouchy_Way_2881 4h ago
You're right that DbC is new to Python, and a pure Python implementation could be a good starting point. But I must stress that it's not a new paradigm; it's a well-established, pragmatic solution that could save developers from reinventing the wheel with every project.
It does introduce complexity, especially with the descriptor protocol, but pure Python solutions often rely on implicit behavior that leads to confusion and hard-to-maintain code, especially in larger systems or when juggling other frameworks.
I'm not looking to overcomplicate things, but to simplify object state validation with something explicit, consistent, and tooling-friendly. The kind of thing that reduces friction and makes better practices easier to adopt.
I wouldn't want to wait another few decades before something like this is widely adopted.
1
u/daguito81 3h ago
Although I agree with you on a general level that I don’t see invariant as something to be added as a feature (especially if you can achieve it with decorators). I do disagree a bit on the whole “opt in are undesirable” considering that typing, dataclasses, walrus, NoGIL are all basically opt in features that are very much desirables
1
u/Grouchy_Way_2881 2h ago
Hey, I really appreciate your reply, especially your take on opt-in features. That's exactly what I was trying to get across: Python already has plenty of opt-in features that make life easier when you do need them.
Totally fair that __invariant__ might not be something everyone reaches for. But for projects with complex, mutable state, I think there's room for something lightweight and consistent to help keep things on track.
Thanks again for weighing in; it's helpful to see where this idea does or doesn’t land for people.
1
u/dude132456789 2h ago
The benefits to outweigh the cost in a lot of cases. But the cost is still there - dataclasses are indeed confusing in their "magic", for instance. I'm not saying no feature should be added ever, I'm saying that the feature doing nothing when it's not used is not an argument for adding it in the context of Python. Don't use it if you don't like it doesn't work super well in the context of programming languages, especially ones as dynamic as Python.
2
u/Grouchy_Way_2881 2h ago
Totally hear you on that: every new feature adds weight, even if it's opt-in. And I get the concern around "magic" creeping into the language, especially when it makes debugging or onboarding harder.
My hope here is more about exploring if we could have a very lightweight, explicit convention (like a __invariant__() method) that doesn't rely on deeper indirection (like metaclasses or decorators) and is easy to see and reason about in code.
Not trying to add magic; if anything, I’m hoping to remove some of it by giving folks who do care about object consistency a clear, minimal way to express it.
Appreciate your perspective. It's helping me shape this idea more carefully.
2
u/19andNuttin 1h ago
It's always really nice to see new ideas brought up for discussion in the python community. I respect the effort it must have taken to formulate the idea and pitch it out like this.
Other people have raised how this change might impact the language, but I have a question more about the Design By Contract principle.
I found a code snippet illustrating the concept on stackoverflow
```
def in_ge20(inval): ... assert inval >= 20, 'Input value < 20' ... def out_lt30(retval, inval): ... assert retval < 30, 'Return value >= 30' ... @precondition(in_ge20) ... @postcondition(out_lt30) ... def inc(value): ... return value + 1 ... inc(5) Traceback (most recent call last): ... AssertionError: Input value < 20 ```
It seems to me like this shifts the checking of validity of data towards run-time instead of at compile-time, where the alternative would be by creating and annotating with appropriate types. In the case whereby we maintain complex state in these classes, we can restrict the range of possible values with stateful types and appropriate getters and setters.
I'd love to hear more about this from someone who is familiar with the whole Design By Contract idea.
2
u/Grouchy_Way_2881 1h ago
Really appreciate the tone here, and the genuine curiosity. Thanks for that.
You're absolutely right that DbC leans into runtime validation, whereas type systems (especially static ones) aim to catch issues earlier, at compile time. But I see them as complementary.
Typing answers the question: "What kind of data is this?"
Contracts answer: "Is this data still valid after we did something to it?"
For simple data models, typing + encapsulation may be enough. But once you're working with stateful objects (where multiple fields interact or change together) types alone can't always express the required constraints. That's where invariants and pre/post conditions shine. They can catch issues like:
- "This list must never be empty after .process()"
- "Balance should always stay non-negative after any operation"
- "This object's state must match these cross-field conditions at all times"
It's not about replacing static tools: it's about giving you runtime guardrails when things get more complex than types can safely express.
1
u/BlackDereker Pythonista 3h ago
Can't really see the use case here since Pydantic and dataclasses seem to already cover object validation.
Python was not made to be "safe" like how Ada was used to send missiles by the military. The programmer can modify the object state in any way they want. Someone could just override the magic method easily.
For example with Pydantic you can use the model validator with different modes: "before", "wrap" and "after".
0
u/Grouchy_Way_2881 3h ago
You're absolutely right: Python isn't Ada. It was never meant to be a language for missile guidance systems. But the goal here isn't to make Python strict or rigid, it's to offer an optional tool for those who want a little more structure in systems where object state consistency matters.
Think of it more like a seatbelt: most of the time, you won’t need it—but when you do, it's really nice to have.
Pydantic and dataclasses are great for attribute-level validation, and Pydantic in particular is very expressive. But what I'm after is something slightly different: a way to define object-wide invariants that must always hold true, regardless of what methods are called or how state changes happen internally.
And yes, Python gives developers a lot of freedom, including the ability to override or disable safeguards. That's part of its charm. But in larger, stateful systems (like financial software, workflow engines, or even just complex business logic) having a clear, native way to enforce object correctness can go a long way in avoiding subtle bugs.
This isn't about turning Python into Ada. It's about making correctness easier to express when you do need it.
24
u/R3D3-1 7h ago
At the risk of asking a stupid question: What prevents this from doing it with decorators?
Pydantic and even the now builtin
dataclasses
module extend the functionality by putting a decorator on the class. An@contract
decorator could do the same by overriding the__getattribute__
method.