Technical
Python Patterns I Reach For Every Week in Year Two
Year one of heavy Python I wrote everything from scratch. Year two I have a small set of patterns I reach for before my fingers hit the keyboard. They cover most problems most of the time.
Pattern 1: Dataclass Config Objects
Every script that takes more than three arguments gets a dataclass config. Not argparse, not environment sprawl. One dataclass, one place to read.
from dataclasses import dataclass
@dataclass
class BatchConfig:
api_url: str
batch_size: int = 50
dry_run: bool = False
def run(config: BatchConfig) -> None:
...The signature is self documenting. The defaults are explicit. Adding a field is one line.
Pattern 2: Pathlib Over os.path
I have not written os.path.join in a year. pathlib.Path is better at everything: joining, reading, globbing, existence checks. If you still use os.path, switching will save small friction on every file operation.
Pattern 3: Generators for Pipelines
Any pipeline with more than two stages gets written as generators, not lists. Memory stays flat, composition stays clean, early termination works naturally.
def read_rows(path):
with open(path) as f:
for line in f:
yield line.strip()
def filter_rows(rows):
for row in rows:
if row.startswith('#'):
continue
yield rowLists collapse on large files. Generators do not.
Pattern 4: Typed Exceptions
I stopped catching bare Exception. Every module defines its own exception class and catches it at the boundary. Makes logs searchable and handlers precise.
class BatchError(Exception):
pass
try:
run_batch(config)
except BatchError as e:
log.error(f"batch failed: {e}")Pattern 5: Single-File Scripts Until Proven Otherwise
Most scripts do not need a package. One file, dataclass config, a main() function, and a __main__ block. The temptation to split into modules on day one is almost always wrong. Split when the file crosses 400 lines or when tests need to import pieces.
Pattern 6: urllib.request for Simple HTTP
Most automation scripts do not need requests or httpx. The stdlib urllib.request handles POST and GET fine, has zero dependencies, and ships everywhere. Reach for heavier clients only when you need retries, async, or connection pooling.
See the official urllib.request docs for the patterns I use.
Pattern 7: Explicit Main Blocks
Every script gets a def main() and a if __name__ == '__main__': main() guard. Importing a script for its helpers should not execute the script. This rule sounds obvious and gets violated constantly. Agents violate it especially often when generating scripts from scratch. Adding it explicitly to my project rules cut a whole class of bugs.
What I Tell Juniors
Learn these six patterns before learning anything else about Python. The industry argues about asyncio versus threads, pydantic versus dataclasses, poetry versus uv. None of those arguments matter until the basic shape of your scripts is clean. These patterns are the basic shape.
The Boring Conclusion
Good Python in year two is indistinguishable from good Python in 2015. The syntax evolved. The shape did not. Consultants who chase the newest pattern every year spend more time retooling than shipping. The durable patterns are durable on purpose.
RELATED READING
The Consulting Shift I Am Making In Year Two
After a year of writing and building, my consulting practice is changing shape. Shorter engagements. Sharper outcomes.
ReadThe Frontend Shift: Shipping Less JavaScript In Year Two
A year ago I reached for Next.js for everything. This year I often reach for nothing.
ReadThe Serverless Lesson I Would Write On A Sticky Note
After a year of shipping serverless projects, one rule explains most of the wins and all of the losses.
Read