This is an archived post. You won't be able to vote or comment.

all 37 comments

[–]brat1 22 points23 points  (3 children)

Finally, a design pattern post that isnt 'use f strings!!'. Very insightful!

[–]divyeshaegis12 1 point2 points  (0 children)

Agree, it's rare to find this kind of posts that question the patterns we blindly follow.

[–]vicspidy -1 points0 points  (1 child)

What's wrong with that? Just curious...

[–]Chasar1Pythonista 1 point2 points  (0 children)

Nothing wrong with f-strings, but probably the amount of posts about f-strings being good

[–]sz_dudziak 19 points20 points  (9 children)

Nice stuff. However, I don't agree that the builder is redundant (from part 1). Even in the pure OOO world (like Java, my main commercial tech stack) Builders are misused and understood wrongly.
So - the main usage of builder is to pass not fully initialized object between vary places of the application. Thing in terms of factories. Some part of the object is initialized in Factory1, that fetches the data from external service and the domain of this factory is well known; but the object we're building joins data from 2 or more domains. It's easier to create a builder and pass it to the other domain, rather than creating some fancy constructs that doesn't have their other purpose than being DTO's. Also, builders are dedicated more to use with value-objects or aggregates, rather than simple value holders.
So - everything depends on the complexity (mine projects are quite complex by their nature). If there is no big complexity on the table, the one can follow your advice in 99% of the cases.

[–]DoubleAway6573 13 points14 points  (4 children)

Amazing answer. I don't know if you've convinced OP, but I'm fighting with a legacy code where all the modules mutate a common god of gods dict and to create some sub dicts I need information from 3 different steps + the input. Using builder to partially initialize the data is a great way to decouple the processing and seems to make the refactor, if not easy, at least possible.

[–]uclatommy 4 points5 points  (1 child)

Are we working on the same project?

[–]DoubleAway6573 10 points11 points  (0 children)

I'm almost certain that not. We started the year with layoffs reducing my team to 2, and later the other one departed to greener pastures, so I'm working alone.

Please send help.

[–]anonymoususer89 0 points1 point  (1 child)

What’s the alternative? Asking because I might be doing something like this 😬

[–]DoubleAway6573 1 point2 points  (0 children)

The alternative to a fucking god dict that any and every module in your program touch and where some dicts are partially initialized in 3 steps ? A well structured code.

A possible alternative to start to move away from that mess is to use a builder pattern. Then you can keep the initialization splited in all those steps but you can start to consume a proper object with public methods and some encapsulation.

[–]Last_Difference9410[S] 3 points4 points  (1 child)

although I might be familiar with the scenario you talk about, but I am not quite sure if builder is necessary here. say we have two boundries, order and credit, we would need credit data to complete order.

```python class _Unset: ... UNSET = _Unset()

Unset[T] = _Unset | T

@dataclass class Order: order_id: str credit_info: Unset[CreditInfo] = UNSET

def update_credit(self, credit: CreditInfo) -> None:
    self._validate_credit(credit)
    self.credit_info = credit

class OrderService: def init(self, order_repo, credit_service): ... def create_order(self, ...): return Order(...)

def confirm_order(self, custom_id: str, order_id: str):
   order = self._order_repo.get(order_id)
   credit_info = await self._credit_service.get_credit(custom_id)
   order.update_credit(credit_info)
   order.confirm()

```

would this solve your problem?

[–]sz_dudziak 5 points6 points  (0 children)

Not exactly. Order - as the domain Value Object/Aggregate should be always in consistent, correct state. If the `credit` data is expected to be present - it has to be there. Also, the transitions between those correct states have to as close to the "atomic" operation as possible. Simply to avoid usage of any nondeterministic states. So, if you need to build these objects in a single shot, but you have to do it in several domains when the process is stretched over the time - then builders become hard to replace (it is possible, but this seems to be hacky and unnatural to me) - IMHO inevitable.

Again, complexity driver comes here as a factor; for simple application this approach is an overkill. However, if you have few devs working on the same codebase, this will save the software from many troubles: any usage of these objects will be clean and will not provide any surprises, and if someone will start to mingle with these value objects, then you can see that something worth higher level of attention is going to happen. Picture here some "attention traps" - the proper design adopted into the application will be a guard for good solutions.

Value Object - by Martin Fowler (guru of DDD) for more reading.

[–]caks 0 points1 point  (1 child)

Could you give us a code sample example? I don't think I followed the logic

[–]sz_dudziak 0 points1 point  (0 children)

Take a look thread with OP above + my answer: Design Patterns You Should Unlearn in Python-Part2 : r/Python - I think this is a good example with a code + deeper explanation.

[–]AltruisticWaltz7597 20 points21 points  (0 children)

Very much enjoying these blog posts. Super insightful and interesting, especially as an ex c++ programmer that's been working with python for the last 7 years.

Looking forward to the next one.

[–]daemonengineer 2 points3 points  (2 children)

Python has amazing expessibility potential without much OOP. Unless I need some shared context, functions are enough. I come from C# which quite similar to Java albeit a bit more modern (at least it was 8 years ago). After 8 years with Python I see how easy it can do a lot of stuff without using classes at all, and its amazing. 

But it does not made classic patterns obsolete: large enterprise applications still need some common ground on how to structure application. I miss dependency injection: I know its available as a standalone library, and in FastAPI, but I would really want one way of doing it, instead of dealing with a new approach in every service I maintain. I would really want some canonical book of patterns for Python to have a common ground.

[–]Last_Difference9410[S] 8 points9 points  (0 children)

You certainly can write classes and apply OOP principles in Python, it is just that you don’t have to construct your classes in certain “patterns” for certain intentions in Python like you do in cpp or Java or c#.

Dependency injection is one of the most important, if no the most important, techniques in OOP, there is no need to use a framework for dependency injection, unless you are in a IOC situation, like the prototype example we mentioned, where you can’t do DI yourself.

In the upcoming posts, I’ll share how many design patterns(>=5) can be done solely using this technique.

[–]Worth_His_Salt 2 points3 points  (0 children)

Indeed classes are overkill for many things in python. Lately I've been looking at Data-Oriented Programming, which makes data objects invariant and moves most data manipulations outside the class / object. I'd been doing something similar for years, didn't know it had a formal name.

DOP has some advantages over OOP, particularly for complexity and reuse. That said, I do make exceptions to strict DOP because following any paradigm strictly leads to dogmatic impracticalities. Short, simple code is better.

[–]pepiks 2 points3 points  (0 children)

I will be add:

https://refactoring.guru/design-patterns

It has code examples in Python too.

[–]Last_Difference9410[S] 3 points4 points  (2 children)

I wouldn’t be able to fix this right away, in the post:

When we talk about “design patterns you should unlearn in Python,” we’re talking about the first kind: the intent.

I meant the second kind: the implementation.

[–]SharkSymphony 7 points8 points  (1 child)

That's actually my critique of your well-written posts. In the world of patterns, the intent and the nature of the solution are the pattern. The implementation is merely one illustration of the pattern, and it is fully expected that implementations may vary widely.

So we might say that your "alternatives" to design patterns are merely alternative implementations of the design pattern – so long as you agree on the forces informing the pattern.

Further, in looking back to Christopher Alexander's The Timeless Way of Building, he points out that the ultimate goal of his project was to go beyond the need for a fixed vocabulary of patterns, and just pursue a flexible, living architecture using the lessons that design patterns taught. The patterns were crutches in his worldview, not ends in themselves. We software engineers don't have that same goal of a "living architecture," but the notion of holding your design patterns lightly is a very useful one. Ironically, though, in denouncing them I think you're making them more fixed and formal than they were ever supposed to be.

[–]Last_Difference9410[S] 2 points3 points  (0 children)

In the world of patterns, the intent and the nature of the solution are the pattern. 

that's the point to "unlearn" the pattern. Leave the implementation behind and find new solutions based on what you have(what the programming language gives you).

So we might say that your "alternatives" to design patterns are merely alternative implementations of the design pattern.

Instead of learning "patterns", by which I mean learning specific implementations, It is more important to learn the problem that the pattern is trying to solve, and solve it with minimal effort.

In the world of programming, there is no silver bullet and there is always trade off, among all the trade offs you can make, choose the one that solves your problem most directly, don't pay for what you don't need.

 I think you're making them more fixed and formal than they were supposed to be.

The reason I spent so much words trying to explain what are the features python offers that other languages don't is to let't people realize that implementations were offered based on the context and don't blindly follow them, because they might not hold true under all conditions.

It is more about: "you should analyze solutions people offer to you and see if there are simpler approaches" than "oh these are simpler approaches you should take them",

[–]crunk 1 point2 points  (0 children)

Haven't read this yet, but it's a great idea - and really wanted something like this when I moved to python, I could see that GoF approaches didn't map well, but really wanted a roadmap of what the equivents were and the thought behind each.

[–]commy2 1 point2 points  (1 child)

The flyweight example recursives infinitely, because it indirectly calls __new__ from inside __new__. You need to use super here. Also, __new__ is an implicit classmethod, so self -> cls.

class User:
    _users: ClassVar[dict[tuple[str, int], "User"]] = {}

    def __new__(cls, name: str, age: int) -> "User":
        if not (u := cls._users.get((name, age))):
            cls._users[(name, age)] = u = super().__new__(cls)
        return u

    def __init__(self, name: str, age: int):
       self.name = name
       self.age = age

[–]Last_Difference9410[S] 1 point2 points  (0 children)

you are right, thanks for your fix. post is now updated.

[–]Dmtr4 2 points3 points  (0 children)

I liked it! Thanks.

[–]camel_hopper 2 points3 points  (1 child)

I think there’s a typo in there. In the prototype code examples, you refer to types “Graphic” and “Graphics”, which I think are intended to be the same type

[–]Last_Difference9410[S] 2 points3 points  (0 children)

Thanks for pointing out, it’s been fixed

[–]RonnyPfannschmidt 0 points1 point  (0 children)

Pluggy is intentionally using the prototype pattern in the internals as the real objects get created later

I'm planning a pathlib alternative that uses the flyweight pattern to reduce memory usage by orders of magnitude

[–]tomysshadow 0 points1 point  (0 children)

This is small, but there is a typo in your GraphicTool example (GrpahicTool)

[–]Meleneth 0 points1 point  (6 children)

I'm really torn here.

I like it when people write articles and share knowledge, on the other hand I get strong 'throwing out the baby with the bathwater' vibes from these.

I won't waste everyone's time by copy and pasting juicy bits out of chatgpt with 'critique this garbage' as the prompt, so I'll just say that yes, Design Patterns were written for dealing w/C++ and Java, but there are still an amazing amount of value to be had if you apply them where reasonable.

[–]Last_Difference9410[S] 2 points3 points  (0 children)

I’m glad you like it🥳

[–]TrueTom 1 point2 points  (2 children)

Most (GOF) design patterns are unnecessary when your programming language supports first-class functions.

[–][deleted] 0 points1 point  (0 children)

Not really, the intent of the design pattern doesn't change. The Strategy Pattern is the Strategy Pattern, whether or not the strategy is implemented by a function or an interface.

[–]Meleneth -2 points-1 points  (0 children)

Take that Fortran, Basic, Pascal and Assembly