Python Habits That Quietly Make Your Code Better
The idioms that separate working Python from Python you enjoy returning to.

The idioms that separate working Python from Python you enjoy returning to.

Python's great virtue is that you can be productive on day one. Its quiet danger is exactly the same thing: it never forces you to learn the idioms, so plenty of people write Python for years in a dialect that is really Java or JavaScript with different punctuation.
The gap is not about obscure tricks. It is a modest set of habits that make code shorter, safer and much easier to return to. Each one takes an afternoon to learn and pays back for the rest of your career.

The clearest marker of Python written by someone thinking in another language is the index loop: creating a counter, incrementing it, and using it to reach into a list. Python's for loop iterates over the items directly.

When you genuinely need the position, enumerate gives you both. When you need to walk two sequences together, zip pairs them. Both are shorter than the manual version and remove the two classic errors: an off-by-one and forgetting to increment.
A list comprehension expresses a transform-and-filter in one readable line, and it is genuinely faster than the equivalent loop with append. Used well, it turns four lines of mechanics into one line of intent.
Used badly, it becomes the worst code in the file. The moment a comprehension carries two nested loops and a conditional, it has stopped being clearer than the loop it replaced. If you cannot read it aloud in one breath, write the loop.
One for and at most one if per comprehension is a good limit. Beyond that, a plain loop with a well-named intermediate variable is easier to read, easier to debug, and easier to add a print statement to at three in the morning.
The with statement guarantees that cleanup happens even when something raises an exception. It is why with open(path) as f is not merely shorter than an open-and-close pair, but strictly safer.
The same pattern applies well beyond files: database transactions, locks, temporary directories, timing blocks, patched settings in tests. Whenever a resource must be released no matter what happens, a context manager is the tool, and writing your own takes a few lines.
This is the single most reliable way to surprise a Python developer who has not met it. A default argument is evaluated once, when the function is defined — not on each call. Give a function a default of an empty list, append to it, and every future call inherits the accumulated contents.
The fix is a one-line habit: default to None, then create the real list or dictionary inside the function body. Do it reflexively and you will never think about it again.
This bug is almost invisible in testing. A single call behaves perfectly; the second call in the same process starts accumulating. It typically survives review, passes tests, and surfaces in production as data appearing where it does not belong.
Passing dictionaries around is fast to write and expensive to maintain. Nothing documents which keys exist, a typo in a key name fails at runtime rather than at edit time, and your editor cannot help you at all.
A dataclass gives you a named type, a readable definition of the fields, autocompletion, sensible equality and a useful string representation — for about the same amount of typing as the dictionary it replaces.
Type hints do not change how Python executes. What they change is everything around it: your editor can autocomplete and catch mistakes as you type, a static checker can find whole classes of bug before you run anything, and the signature tells the next reader what the function actually expects.

You do not have to annotate everything to benefit. Start with function signatures at the boundaries of your modules — the places other code calls into — and let the interior stay untyped if you like. That alone catches a surprising amount.
| Habit | What it prevents | Effort to adopt |
|---|---|---|
| Type hints on public functions | wrong argument types, unclear contracts | minutes per function |
| Virtual environment per project | dependency conflicts, 'works on my machine' | one command |
| Pinned dependency versions | silent breakage on a fresh install | one file |
| A formatter and a linter | style debates, unused imports, obvious slips | one-time setup |
| pathlib instead of string paths | broken paths across operating systems | immediate |
Every Python project gets its own virtual environment. This is not a nicety. Installing packages globally means two projects eventually need different versions of the same library, and the resulting afternoon is one nobody enjoys.
Pin your versions too. An unpinned dependency means the environment you build today and the one your colleague builds next month are different, and you will find out at the least convenient moment.
A data team ran a nightly Python script that had worked for a year. One morning it failed with an error nobody recognised. Nothing in their repository had changed.
A dependency had released a new major version overnight, and the requirements file specified no version at all. The rebuilt container picked up the new release, which had renamed a function they relied on.
The fix took two minutes: pin the version. The investigation had taken most of a day, because everyone reasonably started by looking at code that had not changed.
Use pathlib rather than assembling file paths with string concatenation. It handles separators correctly on every operating system, reads better, and eliminates an entire family of bugs that only appear once someone runs your code on a different platform.

A bare except that swallows everything is the most expensive line in many Python codebases. It hides typos, keyboard interrupts and genuine failures alike, and it converts a clear crash into a mysterious wrong answer later.
Catch the specific exceptions you can actually handle, and let everything else propagate. A traceback at the moment of failure is worth far more than a silent continue.

Iterate over items, use comprehensions sparingly, let with handle cleanup, avoid mutable defaults, prefer dataclasses to loose dictionaries, add type hints at your module boundaries, isolate and pin dependencies, use pathlib, and catch only the exceptions you can handle.
Python rewards learning its idioms more than most languages, because the idiomatic version is usually both shorter and safer. That is an unusual bargain — most trade-offs make you pick one.
Adopt one habit at a time, in real code. Within a few months you will read your old files and see exactly where you started. If you are building data pipelines with these tools, our guide to cleaning messy data is the natural companion.
Tap a star to share what you thought.
No ratings yet
Default arguments are evaluated once when the function is defined, so a default list or dictionary is shared by every call. Mutating it accumulates state between calls. Default to None and create the object inside the function body instead.
When the transformation is a single loop with at most one condition and reads clearly in one line. Once you need nested loops or several conditions, a plain loop with named intermediate values is easier to read and debug.
Because a with-block guarantees cleanup runs even when an exception is raised. That matters for files, database transactions, locks and temporary resources, where a missed close leaks something or leaves state inconsistent.
Sign in to join the conversation.
Loading responses…
Have a story, idea, or something valuable to share? Join The Blog Story for free, publish your content, reach more readers, and earn a share of advertising revenue from eligible content.
Create quality content. Grow your audience. Grow your earning potential.
No. They are ignored at runtime by default. Their value is in editor autocompletion, static checking that catches bugs before execution, and clearer contracts for anyone reading the function signature.
Yes. Without isolation, two projects will eventually require incompatible versions of the same package, and resolving that on a shared global installation is far more painful than creating an environment in one command.
For anything you deploy or run on a schedule, absolutely. Unpinned dependencies mean a fresh install can pick up a new release that changes behaviour, producing failures in code that has not been touched in months.
It catches everything, including typos, interrupts and errors you have no ability to handle. That turns a clear immediate crash into a silent wrong result later. Catch the specific exceptions you can act on and let the rest propagate.
Because pathlib handles path separators correctly across operating systems, offers readable operations for joining and inspecting paths, and removes a whole class of bugs that only appear when someone runs your code on a different platform.