Python: Advanced Concepts
This tutorial covers the parts of Python that separate intermediate programmers from experienced ones. These are not exotic corner cases but the mechanisms that underpin well-designed libraries, frameworks, and large codebases. Understanding them gives you the vocabulary to build expressive, maintainable, and efficient Python software.
Type Hints and Annotations
Type hints let you annotate variables, function parameters, and return values with their expected types. Python does not enforce these at runtime; they are metadata stored in__annotations__ and used by static analysis tools like mypy and Pyright, by IDEs for autocompletion and error highlighting, and by libraries like Pydantic for data validation. The governing module is typing, which provides generic aliases, union types, optional types, and protocol definitions.
Type Hints: Functions, Variables, and Generics
PythonAnnotating signatures with common typing constructs.
TypeVar, Generic Classes, and Literal
PythonWriting reusable generic functions and restricting values with Literal.
dataclasses Module
The @dataclass decorator inspects a class's field annotations and automatically generates __init__, __repr__, and __eq__. This eliminates the repetitive boilerplate of writing these methods by hand. Passing order=True also generates comparison methods; frozen=True makes the class immutable and enables__hash__.
Fields with default values must come after fields without. For mutable defaults (lists, dicts), use field(default_factory=list) rather than assigning a list literal directly, for the same reason you avoid mutable defaults in regular function signatures. The __post_init__ method runs after the generated __init__ and is the place for custom validation or derived field computation.
@dataclass: Basic Usage and field()
PythonAuto-generated methods, defaults, mutable defaults with field(), and __post_init__.
Frozen, Ordered, and Inherited Dataclasses
PythonImmutable dataclasses, comparison ordering, and class inheritance.
Descriptor Protocol
A descriptor is any object that defines __get__, __set__, or__delete__. When a descriptor instance is stored as a class attribute, Python invokes these methods instead of performing a normal attribute access. This is how Python implements properties, class methods, static methods, and slots at a low level. Understanding descriptors gives you the ability to build reusable attribute behaviour that can be shared across any class.
A descriptor that defines both __get__ and __set__ is called a data descriptor and takes priority over the instance's __dict__. A descriptor that only defines __get__ is a non-data descriptor; the instance's __dict__takes priority over it.
Descriptor: Validated Attribute
PythonA reusable positive-number enforcer that works on any class's attributes.
__getattr__, __setattr__, and __delattr__
These three dunder methods let you intercept attribute access at the instance level.__getattr__(self, name) is called only when the normal attribute lookup fails, meaning the attribute was not found in the instance's __dict__ or its class hierarchy. It is the right hook for lazy attribute computation and proxy objects.
__setattr__(self, name, value) is called for every attribute assignment, not just missing ones. Because of this, you must be careful to callobject.__setattr__(self, name, value) inside your override to actually store the value; otherwise, your method calls itself recursively. __delattr__ mirrors this for deletion.
__getattr__: Lazy Attribute Computation
PythonGenerating derived attributes on first access without pre-computing them all.
__setattr__: Intercepting All Assignments
PythonLogging every attribute change and validating the value before storing it.
Metaclasses
In Python, everything is an object, including classes. A class is an instance of its metaclass. The default metaclass is type. When Python processes a class definition, it calls the metaclass to create the class object. By defining a custom metaclass, you can intercept this creation and modify the resulting class: enforce naming conventions, validate that certain methods are present, automatically register subclasses, or inject methods.
Metaclasses are a powerful tool, but they should be used sparingly. Class decorators can handle many of the same use cases with less complexity. Reach for a metaclass when you need to intercept the class creation process itself, particularly when you need logic to run at class definition time rather than at instantiation time.
Metaclass: Auto-registering Subclasses
PythonA plugin registry that automatically tracks every subclass at definition time.
Metaclass __init_subclass__: A Simpler Alternative
PythonFor many registration patterns, __init_subclass__ achieves the same result with less ceremony.
__new__ vs __init__
__new__(cls, *args, **kwargs) is the static method responsible for creating a new instance. It receives the class itself as its first argument and must return the new object. __init__(self, *args, **kwargs) then receives that already-created object and sets up its state. For most classes you never need to override __new__, but it is essential for immutable types (where __init__ cannot change the value after creation), for implementing Singletons, and for customising instance creation in metaclasses.
__new__ on Immutable Types: Custom int Subclass
PythonFor immutable built-ins, initialisation must happen in __new__ because __init__ runs too late.
__new__ for Singleton Pattern
PythonEnsuring only one instance of a class ever exists.
Context Managers and the contextlib Module
A context manager is any object that implements __enter__ and __exit__. The with statement calls __enter__ on entry and __exit__ on exit, even if an exception occurs. __exit__ receives the exception type, value, and traceback as arguments; returning a truthy value suppresses the exception.
The contextlib module provides utilities that make context managers easier to write and compose. contextlib.contextmanager turns a generator function with a single yield into a context manager, with code before the yield acting as__enter__ and code after acting as __exit__. Other useful tools includecontextlib.suppress for silently ignoring specific exceptions,contextlib.redirect_stdout for capturing output, andcontextlib.ExitStack for dynamically composing multiple context managers.
Class-based Context Manager
PythonA timer context manager that measures and prints elapsed time.
contextlib.contextmanager
PythonTurning a generator function into a context manager with decorator syntax.
contextlib: suppress, redirect_stdout, ExitStack
PythonThree utilities that handle common patterns with minimal boilerplate.
Abstract Base Classes (ABCs) in Depth
The abc module provides ABC and abstractmethod. A class that inherits from ABC and declares methods with @abstractmethod cannot be instantiated directly; any concrete subclass must implement all abstract methods, or it too becomes abstract. Python raises a TypeError at instantiation time rather than at method-call time, catching the error early.
ABCs also support virtual subclasses via register(). Registering a class tells Python to consider it a subclass for isinstance() and issubclass() checks, without requiring actual inheritance. This lets existing classes participate in type-checking hierarchies retroactively. The collections.abc module uses this extensively to make built-in types like list passisinstance([], collections.abc.Sequence).
Defining and Implementing an ABC
PythonAn abstract Shape class enforcing that every subclass provides area and perimeter.
Protocols and Structural Subtyping
A typing.Protocol defines an interface by specifying which methods and attributes a conforming object must have. Unlike ABCs, a class does not need to inherit from or register with a Protocol to satisfy it. If it has the required methods with compatible signatures, it satisfies the Protocol structurally. This is sometimes called "duck typing with type checker support."
Protocols are especially valuable when you want to accept any object with a certain shape rather than requiring a specific base class. You can also use@runtime_checkable to enable isinstance() checks against the protocol at runtime, though only method and attribute presence (not signatures) is verified.
typing.Protocol: Structural Subtyping
PythonAny object with the required methods satisfies the Protocol, no inheritance needed.
Mixin Classes
A mixin is a class designed to be mixed into other classes via multiple inheritance, adding a specific slice of behaviour without being a meaningful standalone class itself. Mixins usually have no __init__ and do not hold significant state of their own. They depend on the class they are mixed into having certain attributes or methods, which they document via type annotations or abstract methods.
Python's standard library uses this extensively: socketserver.ThreadingMixInand socketserver.ForkingMixIn add concurrency handling to server classes. Django uses mixins throughout its class-based views. The key discipline is to keep mixins small and focused on a single responsibility.
Mixin Classes: Composable Behaviour
PythonAdding serialisation, validation, and logging capabilities to model classes via mixins.
Memory Management and the gc Module
CPython manages memory primarily through reference counting. Every object tracks how many references point to it. When the count drops to zero, the object is immediately deallocated. This handles the vast majority of objects automatically. The problem is cyclic references: if object A holds a reference to B, and B holds a reference to A, neither count ever reaches zero even when both are unreachable from the program.
The gc module provides a cyclic garbage collector that periodically detects and breaks reference cycles. It organises objects into three generations based on how long they have survived. New objects start in generation 0; objects that survive a collection cycle are promoted. You can trigger collection manually, inspect tracked objects, and disable the collector if your program guarantees no cycles.
gc Module: Reference Cycles and Manual Collection
PythonDetecting cycles, inspecting collected objects, and measuring reference counts.
Weak References
A weak reference to an object does not prevent it from being garbage collected. When the only remaining references to an object are weak references, the object is collected and the weak references are automatically invalidated. This is useful for caches, where you want to cache objects but not prevent memory from being freed when the cache is the only thing keeping them alive.
weakref.ref(obj) creates a weak reference. Calling the reference object returns the original object if it is still alive, or None if it has been collected.weakref.WeakValueDictionary is a dict that holds weak references to its values, automatically removing entries when values are collected.
weakref: Weak References and WeakValueDictionary
PythonBuilding a cache that does not prevent garbage collection of its entries.
copy Module: copy() and deepcopy()
Assignment in Python creates another reference to the same object, not a new object. To create an independent copy, you use the copy module. copy.copy(obj)makes a shallow copy: it creates a new top-level container but the nested objects inside it are still shared with the original. copy.deepcopy(obj) recursively duplicates every object in the structure, producing a fully independent copy.
You can customise how your class is copied by implementing __copy__ and__deepcopy__. The __deepcopy__ method receives a memo dictionary that tracks already-copied objects to handle cyclic structures correctly.
Shallow vs Deep Copy
PythonDemonstrating the difference with a nested mutable structure.
Custom __copy__ and __deepcopy__
PythonControlling how your class is copied.
Pickling and Serialisation
The pickle module serialises Python objects to a byte stream and deserialises them back. Unlike JSON, pickle can handle almost any Python object: custom class instances, functions, lambdas (with some restrictions), numpy arrays, and more. The protocol version controls the format; protocol 5 (Python 3.8+) is the most efficient for large data.
The critical security rule: never unpickle data from an untrusted source. The deserialisation process can execute arbitrary code. If you need to exchange data with external systems or untrusted parties, use JSON, MessagePack, or another format that does not allow code execution.
pickle: Serialising and Deserialising Objects
PythonSaving a custom object to bytes and restoring it, with file-based persistence.
Customising Pickle with __getstate__ and __setstate__
PythonControlling what gets serialised and how state is restored, for example excluding cached or transient data.
Serialisation Format Comparison
Choosing the right serialisation format depends on the use case, the audience, and the security context.
- pickle: Python-native, supports almost all types, fast, but Python-only and unsafe with untrusted data.
- json: human-readable, cross-language, safe; limited to dicts, lists, strings, numbers, booleans, and None.
- json with custom encoder/decoder: extends JSON to handle dates, UUIDs, and custom types while staying text-based.
- shelve: a persistent dict backed by pickle; convenient for simple key-value storage in scripts.
- dataclasses.asdict() + json: a clean pattern for serialising dataclass instances to safe JSON.
Quiz - Test Your Knowledge
Ten questions covering type hints, dataclasses, the descriptor protocol, attribute access hooks, metaclasses, object creation with __new__, context managers, abstract base classes, protocols versus ABCs, and the copy module. Several questions require understanding subtle distinctions, so read each option carefully.
Knowledge Check
1. What is the purpose of type hints in Python, and do they affect runtime behaviour?
2. What does @dataclass automatically generate for a class?
3. In the descriptor protocol, when is __get__ called with obj=None?
4. What is the difference between __getattr__ and __getattribute__?
5. What is a metaclass in Python?
6. What is the key difference between __new__ and __init__?
7. What does @contextlib.contextmanager allow you to do?
8. What is the purpose of Abstract Base Classes (ABCs)?
9. How does typing.Protocol differ from inheriting from an ABC?
10. What is the difference between copy.copy() and copy.deepcopy()?