Python logo climbing staircase with rising arrow and code editor window

7 Advanced Python Tricks That Use What the Language Already Promises You

Levelling up in Python rarely means learning new syntax. It means learning what the language already promised you.

That is the argument worth taking seriously, because most “advanced Python” content sells something else: a third-party library, a clever one-liner, a pattern borrowed from a language you are not writing. The seven techniques below require no dependencies at all. Every one is a built-in or a standard-library contract that has been sitting in the documentation, quietly guaranteeing behaviour you can rely on.

Two of them need Python 3.11 or later, and that is flagged where it applies. Everything else works on any version you are likely to be running in production.

The interesting part is not the trick. It is the edge attached to it. Each of these tools has a sharp one, and knowing where it is happens to be the difference between code that reads well and code that fails in a way you cannot debug at 2am.

Trick 1: iter() with a sentinel

Most of us meet iter() as the thing that turns a list into an iterator. It has a second form that is far more useful in production code: pass a zero-argument callable plus a sentinel value, and Python will call the function repeatedly, stopping when a return value equals the sentinel.

The classic read loop becomes shorter and, more importantly, harder to get wrong:

for chunk in iter(lambda: stream.read(64), b""):
    process(chunk)

A 200-byte stream yields chunks of 64, 64, 64 and 8 bytes, then stops because read() returned the empty-bytes sentinel. No while True, no break, no chance of forgetting the break and looping forever. The same shape applies to database cursor batches and queue messages, wherever a producer signals exhaustion with a known value.

The callable must take zero arguments. If your read function needs a size, a timeout or a connection handle, wrap it in a lambda or a functools.partial first. Passing a function that expects arguments will not fail politely at the call site; it will fail when iter() tries to invoke it.

Trick 2: ExitStack for runtime-decided resources

Fixed nested with statements handle a fixed number of resources. They cannot handle a list of files chosen by a user at runtime, which is exactly when cleanup matters most.

contextlib.ExitStack fills that gap. You register resources as you acquire them, and every one closes when the block exits, including on exceptions. Cleanup runs in reverse order of entry, so three registered trackers close as 2, 1, 0, which is the ordering you want when later resources depend on earlier ones.

from contextlib import ExitStack

with ExitStack() as stack:
    files = [stack.enter_context(open(p)) for p in paths]
    process(files)

enter_context() accepts anything with a context-manager interface, so files, locks and network clients can all share one cleanup guarantee. That uniformity is the real win: you stop writing bespoke teardown paths for each resource type.

When the resource count is fixed and small, keep ordinary with. It reads better, and readability is not a consolation prize. ExitStack earns its place when the count is genuinely dynamic.

Trick 3: memoryview and the buffer that fights back

Slicing bytes copies. On large packets or image buffers processed in a loop, those copies cost real memory and real time. A memoryview exposes the same underlying buffer without copying, and a writable view writes straight through to the original.

Two things to know, and the second is the interesting one.

First, the performance gains are workload-specific. Measure before celebrating, because a view adds indirection and the win depends on how much slicing you actually do. If you are batching work for a model, this is the same lesson that shows up in Length-Bucketed Batching: How to Stop Wasting GPU Cycles on Padding for Small Language Models, where the waste is not in the algorithm but in how the data is shaped before it reaches the hardware.

Second, an exported view pins the buffer. Resizing a bytearray while a view is alive raises BufferError until release() is called:

buf = bytearray(b"hello")
view = memoryview(buf)
buf.extend(b" world")   # BufferError: Existing exports of data: object cannot be re-sized
view.release()
buf.extend(b" world")   # fine now

That sounds like an annoyance. It is better described as a feature in disguise: it catches lifetime bugs loudly at the point of the mistake, rather than letting a stale pointer corrupt data silently somewhere downstream.

Trick 4: Exception groups and except* (Python 3.11 and later)

Since Python 3.11, exception groups carry multiple coexisting failures instead of forcing you to report the first error and lose the rest. The matching except* syntax routes each subgroup separately. In a batch validation run, a ValueError handler might see two row failures while an OSError handler sees the disk problem, and both are handled in one pass. Unmatched failures keep propagating, so nothing is swallowed by accident.

try:
    validate_batch(rows)
except* ValueError as eg:
    for exc in eg.exceptions:
        log.warning("bad row: %s", exc)
except* OSError as eg:
    raise BatchIOFailure() from eg

Two naming details worth committing to memory, because they trip people up in tracebacks. The class hierarchy is BaseExceptionGroup at the root, with ExceptionGroup as the subclass that can only hold Exception instances. except* never binds a single exception: the name you write always refers to a group, even when exactly one leaf exception matched. And the version floor is hard. except* is a syntax error before 3.11, so a library that uses it cannot claim support for 3.10 or earlier.

The discipline is in when not to use it. Exception groups are for cases where several failures genuinely coexist: concurrent tasks and batch validation are the two classic examples. A single failure with a known cause still deserves a plain raise. Wrapping one exception in a group to look sophisticated makes tracebacks worse for everyone who reads them after you.

Trick 5: ChainMap and the merge you cannot undo

Configuration usually arrives in layers: defaults, then a config file, then environment variables, then command-line flags. Merging them into one dictionary is irreversible, and irreversible is the wrong property for configuration you may need to inspect or override later.

ChainMap keeps the layers separate and searches them in order. It is a live view, so updating defaults later is instantly visible through the chain in a way a merged copy cannot offer. new_child() pushes a fresh layer onto the front for scoped overrides, leaving everything beneath untouched.

from collections import ChainMap

defaults = {"retries": 3, "timeout": 30}
cfg = ChainMap(cli_flags, env_vars, defaults)

cfg["retries"] = 5      # lands in cli_flags only
print(defaults["retries"])  # still 3

Writes and deletes go to the first mapping only. That is correct override semantics, and it is surprising if you expected a merge. When you genuinely want a frozen snapshot, use the | merge operator instead.

Trick 6: MappingProxyType as an API signal

Returning an internal dictionary from a class hands every caller a remote control for your object’s state. MappingProxyType returns a read-only view instead. A consumer attempting registry["json"] = ... gets a TypeError, while your class’s own code keeps writing to _registry and authorised changes show through the proxy immediately.

from types import MappingProxyType

class Registry:
    def __init__(self):
        self._registry = {}
        self.view = MappingProxyType(self._registry)

    @property
    def entries(self):
        return self.view

Why not return a copy? Because a copy goes stale the moment the registry changes, and stale configuration is worse than no configuration. The proxy stays current for free.

The protection is shallow, so a mutable value inside the mapping is still mutable. This is an API-clarity tool, not a security boundary, and a determined caller can reach the underlying dictionary. Treat it as documentation that the interpreter enforces.

Trick 7: functools.partial and the leftmost-argument problem

functools.partial() has always frozen arguments from the left. That is genuinely useful when the argument you want to fix is the first one, and useless when it is not. If you want to pin the third parameter of a function, partial will not do it.

from functools import partial

def fetch(url, timeout, retries, headers=None):
    ...

# This works: url is the leftmost argument.
fetch_home = partial(fetch, "https://example.com", retries=2)

# This does not pin timeout for you. The positional
# "https://example.com" is still consumed by url first.
broken = partial(fetch, retries=2)

# Write the wrapper instead.
def fetch_with_retries(url, timeout=10, headers=None):
    return fetch(url, timeout, 2, headers)

The workaround is a small wrapper function or a lambda with explicit keyword arguments, and both are four lines of ordinary Python that read better than a clever workaround. The leftmost-argument constraint is the caveat, and the technique is knowing when to reach for partial and when to write the wrapper.

One related tool worth knowing: functools.partialmethod brings the same leftmost-binding behaviour to methods on a class, which is where partial alone tends to produce confusing descriptors.

Adopting these contracts without hurting yourself

Three habits make the difference between using these tools well and using them to look clever.

Measure before optimising. memoryview and iter() with a sentinel both change performance characteristics, and neither is automatically faster in your workload. Profile first, then reach for the tool.

Prefer plain with and plain raise when they read better. ExitStack and exception groups are solutions to specific shapes of problem, not general upgrades. A fixed pair of files does not need a stack, and one known-cause failure does not need a group.

Treat each sharp edge as part of the technique. The zero-argument rule for iter(), the pinned buffer behind memoryview, the first-mapping-only writes in ChainMap, the shallow protection of MappingProxyType: these are not footnotes. They are the contract. Learn the contract and the built-ins stop being trivia and start being infrastructure.

We covered numba tricks python runtime in more detail elsewhere.

Similar Posts