
Begin
14 pages · ~28 min
Advanced Python Programming Patterns
This advanced Python course helps experienced developers master complex patterns to write more efficient, maintainable, and scalable code.
A digital instructor presents all 14 pages. Hold “Ask” at any point and ask out loud — the answer comes from this course. No sign-up needed.
What you’ll learn
- 01Advanced Patterns in Python Programming: From Working Code to Reviewed, Scalable DesignWelcome. This course is about moving past syntax fluency into design that survives review and scale. You already know Python. The question is whether your code stays changeable when features pile up and several engineers touch it. Working code is a starting point, not a finish line. Across these slides, we examine four lenses: the data model, control flow, structure, and performance. Each one gives you a vocabulary for naming what is wrong and defending what you changed. We will look at patterns as references, not templates. A pattern is useful when it matches the shape of your problem. Applied blindly, it hides complexity instead of reducing it. We will use one running example service throughout, so each pattern connects to real code rather than isolated exercises. And we will practice naming and defending patterns in code review, because that is where design decisions become visible to your team. By the end, you should be able to choose a pattern deliberately, say when it is appropriate, and say when it adds unnecessary complexity. Next, we cover the baseline, your goals, and the pattern taxonomy.
softwarepatternslexicon.comsoftwarepatternslexicon.comquantifiedcode.github.io+22 min - 02Baseline, Goals, and the Pattern TaxonomyLet us set the baseline for this session. You are comfortable with functions, classes, decorators, virtual environments, and at least one testing framework. That is the starting point. Now, the failure modes we will target. Implicit state that hides dependencies. Leaky abstractions that force callers to know internals. Accidental quadratic behavior in loops. Untyped boundaries where data crosses modules or services. These are common in backend systems and data pipelines, and they are expensive to review. To organize the patterns, we will use six categories: data-oriented, structural, behavioral, concurrency, typing, and performance. Each pattern earns its place only when it reduces a real cost. When you review code, apply four lenses: naming, boundaries, error handling, and testability. A pattern that improves performance but harms readability may not be worth it. A pattern that clarifies boundaries is usually worth the trade. Finally, scope. No framework tours. No language rebuilds. No pattern laundry. We will focus on patterns that survive production and code review. Next, we look at the data model as a pattern foundation.
2 min - 03The Data Model as a Pattern FoundationLet's ground these patterns in the data model itself. The protocols worth knowing are __getattr__, __setattr__, __call__, __iter__, __enter__, and __exit__. Each one lets your objects participate in Python's core behavior. Now, descriptors and properties: they make attribute access carry behavior. You assign a descriptor to a class attribute, and its __get__ and __set__ methods run every time that attribute is touched. The precedence rule matters here. Data descriptors, which define __set__ or __delete__, beat instance dictionaries. Non-data descriptors, with only __get__, do not. That explains why property intercepts writes while methods can be shadowed per instance. Next, metaclasses versus __init_subclass__ versus decorators. Pick the lightest tool that solves your problem. For contextlib, @contextmanager and ExitStack are usually cleaner than manual __enter__ and __exit__. Just be deliberate about exception suppression. Finally, watch for pitfalls: surprising attribute behavior, MRO conflicts, and singletons that fight testing. Choosing Among Composition, Inheritance, and Customization.
docs.python.orgdocs.python.orggithub.com+21 min - 04Choosing Among Composition, Inheritance, and CustomizationLet's move on. When you extend behavior, the real question is which mechanism to reach for. Start with mixins, but keep them small and testable. Deep mixin chains create method resolution order surprises that are expensive to debug in review. For interchangeable algorithms, prefer strategy and command as callables typed with typing.Protocol. You get structural typing and testability without building a deep hierarchy. For extensibility across packages, use registries with entry points and importlib discovery, so third parties register themselves without editing your code. When customizing classes, escalate deliberately. Reach for a class decorator first, then __init_subclass__, and only then a metaclass. Lightest tool first. And remember, composition usually beats inheritance, and a plain function often beats both. Choose the simplest mechanism that solves the problem, and be able to justify it in terms of readability, performance, maintainability, and review cost. Next, we'll look at plugin discovery, entry points, and boundaries.
packaging.python.orggithub.comdocs.python.org+21 min - 05Plugin Discovery, Entry Points, and BoundariesNow let's look at plugin discovery and the boundaries around it. There are three common discovery routes. First, naming conventions: you import every module that matches a prefix, the way Flask finds modules named flask underscore something. Second, namespace packages: you make a subpackage like myapp dot plugins a namespace, and call pkgutil.iter_modules on its path. Third, metadata entry points. For that, importlib.metadata.entry_points is the current standard. You declare a group in the plugin's packaging metadata, then load it with discovered dot load. Whichever route you choose, wrap plugin import and initialization in defensive error handling. Catch the exception, log which plugin failed, and degrade gracefully instead of crashing the whole service. Also expose only the objects plugins are allowed to touch, and decide explicitly who owns the plugin lifecycle. Finally, choose deliberately. Dynamic imports suit internal extensions. Entry points fit separately installed distributions. An explicit config list makes debugging easier because the loaded set is never a surprise. Next, dependency injection without a framework.
packaging.python.orggithub.comdocs.python.org+21 min - 06Dependency Injection Without a FrameworkNow let's look at dependency injection without a framework. In plain Python, you can apply ports and adapters by defining explicit interfaces and owning object lifetimes yourself. Wrap vendor SDKs behind adapters, so vendor specific shapes never leak into your business logic. That keeps your domain code stable when a provider changes. The practical payoff is stable seams for tests. Inject clocks, ID generators, configuration, repositories, and clients instead of importing them globally. This makes unit tests fast and deterministic, with no patching of module internals. For wiring, choose deliberately. Constructor injection works well when dependencies are few and explicit. An application factory centralizes setup for larger services or command line entry points. An explicit context object helps when many related dependencies travel together. Each adds indirection, so weigh readability and review cost. Also weigh the typing approach. Structural typing with Protocol is lightweight and decoupled. ABCs give you explicit contracts and runtime enforcement. Runtime checks catch wiring errors early but add startup cost and boilerplate. Pick the lightest option your team can review confidently. Next, we move on to functional building blocks in production code.
cubic.devpython-coding-guidelines.readthedocs.ioneudesic.github.io+22 min - 07Functional Building Blocks in Production CodeLet's look at functional building blocks that hold up in production code. First, first-class functions and closures. They're powerful, but watch the late-binding trap in loops. If you capture a loop variable inside a lambda, every closure sees the final value. Bind it with a default argument, or use functools partial. Next, functools gives you partial, lru_cache, cache, singledispatch, and wraps. These are small, well-understood tools. They reduce boilerplate and make intent explicit. Generators and itertools let you build lazy pipelines for batching, grouping, and windowing. They keep memory flat when data streams are larger than RAM. One caution. Unbounded caches grow memory. Bound them with maxsize and plan invalidation. A stale cache is a production incident. Use functional style when it clarifies intent, not when it hides it. If a reviewer has to trace three decorators to understand one call, favor a plain function. In code review, ask what each pattern solves, and whether a simpler construct would read better for your team. Next, we move to structural concurrency with task group and cancellation.
cubic.devpython-coding-guidelines.readthedocs.ioneudesic.github.io+22 min - 08Structural Concurrency with TaskGroup and CancellationLet's talk about structural concurrency in asyncio. First, match the workload shape to your execution model. I O bound work fits asyncio, blocking libraries fit threads, and C P U bound work fits processes or a task queue. TaskGroup, added in Python three point eleven, gives you structured concurrency. You create tasks with create task inside the async with block. All tasks are awaited when the context manager exits, and the group holds strong references, so nothing disappears mid execution. If one task fails, the remaining siblings are cancelled, and the failures combine into an ExceptionGroup. That gives you reliable cleanup and clear error aggregation. One caution. Never swallow CancelledError. Timeout and group cancellation both rely on it. If you must suppress it, call uncancel to clear the state. Finally, bound your queues for backpressure, and keep blocking calls off the event loop. Next, we move into async refactoring, observability, and async anti patterns.
2 min - 09Async Refactoring, Observability, and Async Anti-PatternsNow let's refactor async code with observability in mind, and name the anti-patterns that break it.
First, blocking libraries inside an async def stall the entire event loop. A synchronous database driver or a requests call blocks every other task. Move it to a thread executor, or choose an async-native client.
Second, untracked background tasks can vanish. The event loop holds only weak references. A fire-and-forget task may be garbage collected before it finishes, and its exception is lost. Use a TaskGroup, which keeps strong references and propagates failures. If you truly need fire-and-forget, store the task in a set and discard it on completion.
Third, swallowing CancelledError breaks structured concurrency. TaskGroup and asyncio.timeout are built on cancellation. If you catch CancelledError and do not re-raise it, those primitives misbehave. Let it propagate.
Fourth, shared mutable state across tasks needs locks or coordination. Concurrency does not remove races. Protect shared structures with asyncio.Lock, or redesign to avoid sharing.
For observability, set correlation IDs, name your tasks, and log in structured form. Propagate timeouts and cancellation through every layer. And be honest about scope: sometimes the simplest fix is to skip async entirely.
Next, we move to typing and data modeling at the boundaries.
2 min - 10Typing and Data Modeling at the BoundariesLet's now look at typing and data modeling at the boundaries. Protocols enable structural subtyping. Instead of annotating a concrete class, you annotate the behavior you depend on. Your argument accepts anything with the right methods. That reduces coupling and makes testing simpler. Type variables give you reusable generics. ParamSpec goes further, preserving the signature of a decorated function, so wrappers stay transparent to type checkers. At the seam, where data enters your system, use TypedDict and frozen dataclasses to model untrusted input. Validate once at the boundary. After that, let static types govern everything downstream. Gradual typing is the practical strategy. Annotate seams first. Keep Any contained and visible, so reviewers can see where dynamic behavior lives. The tradeoff is explicit. More precise boundaries cost annotation effort, but they buy safer refactors and clearer reviews. Next, we will examine anti-patterns and how to refactor toward stronger designs.
2 min - 11Anti-Patterns and Refactoring to Stronger DesignsNow let's talk about anti-patterns, and how to refactor toward stronger designs.
Start with God objects and huge modules. When one class or module owns too many responsibilities, every feature touches the same file, and merge conflicts become routine. The fix is not random splitting. Find responsibility clusters, then extract them one at a time.
Hidden global state is next. Mutable module-level caches, clients, and configuration leak dependencies across your code and break test isolation. Pass context explicitly, or use an application factory.
Deep inheritance and method resolution order surprises are a third source of pain. Behavior appears from unexpected places, producing spooky action at a distance. Prefer composition, or a protocol with a narrow interface.
And beware over-engineered abstraction. A strategy registry where a plain function suffices adds friction, not clarity.
The refactoring sequence is deliberate. Characterize current behavior with tests. Extract one concept. Inject dependencies through a seam. Isolate I O behind small interfaces. Pin behavior with focused tests before each step.
For code review, watch for three heuristics. Repeated edits in the same area. Brittle tests that assert implementation details. And vendor S D K leakage into domain code.
Next, we turn to performance, profiling, and optimization patterns.
softwarepatternslexicon.comsoftwarepatternslexicon.comquantifiedcode.github.io+22 min - 12Performance, Profiling, and Optimization PatternsLet's talk about performance, profiling, and optimization patterns. The first rule is simple. Measure first. Use cProfile, timeit, or a sampling profiler. Never guess where the bottleneck is.
In real code, complexity often hides. An accidental quadratic loop, or repeated work inside a loop, can dominate runtime. A cache can help, but only with a clear invalidation strategy. Memoization for pure functions, request-scoped caches for per-call data, and explicit expiry for shared state.
For memory, prefer __slots__, generators, and streaming over bulk loading. When you reach for acceleration, consider C extensions, Cython, or Rust. But only after measuring. If the hot path is small and isolated, a native extension may be worth the maintenance cost. If not, it adds complexity your team will own forever.
So the takeaway is this. Profile with evidence, cache with discipline, and optimize only where the data says it matters. Next, we'll look at pattern language for reviews, teaching, and team standards.
cubic.devpython-coding-guidelines.readthedocs.ioneudesic.github.io+21 min - 13Pattern Language for Reviews, Teaching, and Team StandardsLet's close the loop and turn these patterns into a shared language for your team. In code review, name the pattern and the trade-off explicitly. Instead of writing that something feels wrong, write that this fallback hides the failure, or that this abstraction adds indirection without payoff. Your review checklist should cover the areas tools cannot judge: boundaries, error handling, concurrency, typing, and testability. Ruff and mypy handle formatting and type errors, but a reviewer decides whether an exception path is safe. When you teach, use examples and counterexamples. Show a mutable default argument, then show the fix. Show a blocking call inside an async handler, then show the timeout. Deliberate practice beats reading patterns. Codify conventions lightly. Add a rule when it prevents real defects, and keep room for innovation. Adopt through pilots, short docs, and honest metrics. Track defects caught, not lines reviewed. A pattern language is a tool for judgment, not a substitute for it. That prepares you for the workshop ahead: Capstone Workshop: Applied Patterns in a Small Service.
cubic.devpython-coding-guidelines.readthedocs.ioneudesic.github.io+22 min - 14Capstone Workshop: Applied Patterns in a Small ServiceThis is our capstone, so let's put everything together. Your task is to build a small event-driven service with a data pipeline. Apply composition, TaskGroup concurrency, typing, and caching. Then, deliberately hunt the anti-patterns you introduced during the exercise. For example, did you create a God object, or hide a mutable default argument? Once you find them, review your own code as a reviewer would. Justify each pattern choice in terms of readability, performance, maintainability, and team review. Finally, debrief as a group. Decide what to carry into your real repositories next week. That might be characterization tests before a refactor, explicit dependencies instead of hidden globals, or a protocol where a vendor SDK used to leak. Keep the changes boring and incremental. Thank you for working through these advanced patterns with me. You now have a practical toolkit for writing Python that is easier to change, test, and review. Go apply one pattern this week, and keep evaluating them critically.
softwarepatternslexicon.comsoftwarepatternslexicon.comquantifiedcode.github.io+22 min
Take the deck with you
Download this course as a file — free, no sign-up needed.
- PDF handoutEvery slide page, ready to print or share.15 pages · 3.3 MBDownload
- Narrated PowerPointThe deck that presents itself — every slide carries the digital human's narration video.15 pages · 15.3 MBDownload
- PowerPoint slidesThe full deck as a .pptx — open it in PowerPoint, Keynote, or Google Slides.15 pages · 3.2 MBDownload
Free to use in your own training — please keep the PersonWise credit page at the end.
Have your own deck? Turn it into a course
Sources consulted
Web sources consulted while building this course.
- Refactoring Python Anti-Patterns | Software Patterns Lexicon — softwarepatternslexicon.com
- Refactoring with Design Patterns | Software Patterns Lexicon — softwarepatternslexicon.com
- Python Anti-Patterns — quantifiedcode.github.io
- python-antipatterns.md at master · charlax/antipatterns — github.com
- docs/code-smells-guide.md at main · cheickmec/smellcheck — github.com
- Descriptor Guide — Python 3.14.5 documentation — docs.python.org
- 3. Data model — Python 3.14.5 documentation — docs.python.org
- Doc/howto/descriptor.rst — github.com
- Descriptor Protocol ¶ — python-reference.readthedocs.io
- Descriptor HowTo Guide — Python 3.11.15 documentation — docs.python.org
- Entry points specification - Python Packaging User Guide — packaging.python.org
- source/guides/creating-and-discovering-plugins.rst — github.com
- importlib.metadata – Accessing package metadata — Python 3.14.6 documentation — docs.python.org
- Best practices for managing dynamic imports in plugin-based architecture? — discuss.python.org
- importlib — The implementation of import — Python 3.14.5 documentation — docs.python.org
- Python code review standards and best practices | cubic — cubic.dev
- Python Code Review Checklist — python-coding-guidelines.readthedocs.io
- Python Code Reviews - Neudesic Engineering Excellence Playbook — neudesic.github.io
- skills/python-code-review/references/review-checklist.md — github.com
- Python Code Quality | Checklist — kodus.io