Python – Best Practices

August 17, 20264 min readUpdated 8/20/2026

The habits that make Python read like Python. None of this is about cleverness — it is about code someone else can pick up, which after a few months includes you.

PEP 8, enforced by a tool

PEP 8 is the style guide: four spaces, snake_case for functions and variables, CapWords for classes, UPPER_CASE for constants, lines under about 88 characters.

Do not memorise it and do not argue about it. Install a formatter and let it decide:

pip install ruff

ruff format .        # reformat everything
ruff check .         # find problems
ruff check --fix .   # fix the ones it can

Ruff does formatting and linting in one fast tool and has largely replaced black, flake8 and isort. Run it on save, or in a pre-commit hook. A team that formats automatically never has a style discussion again, which is the actual benefit.

Name things for what they are

# Hard to read
def calc(l, d):
    return [x * (1 - d) for x in l]

# Easy to read
def apply_discount(prices, discount_rate):
    return [price * (1 - discount_rate) for price in prices]

print(apply_discount([100.0, 250.0], 0.1))     # Output: [90.0, 225.0]

Both work. Only one tells you what a discount rate is. Single letters are fine for a coordinate or a loop index and nowhere else — and l in particular is banned by every linter because it is indistinguishable from 1 in many fonts.

Booleans read best as questions: is_active, has_overdraft, can_withdraw. Functions that do something get a verb; functions that answer something get a question.

Ask forgiveness, not permission

config = {"currency": "USD"}

# Look before you leap
if "overdraft" in config and isinstance(config["overdraft"], int):
    limit = config["overdraft"]
else:
    limit = 0

# Easier to ask forgiveness
try:
    limit = int(config["overdraft"])
except (KeyError, TypeError, ValueError):
    limit = 0

print(limit)     # Output: 0

Python's convention is to attempt the operation and handle the failure, rather than testing every precondition first. It is shorter, and it has no gap between the check and the use — in the first version the value can change in between, and the check has to duplicate what the conversion already knows.

The check-first style is right when failure is expensive or the check is genuinely cheaper. For dictionary access and type conversion, try it.

Comprehension, then loop, then map

prices = [100.0, 250.0, 40.0]

print([p * 0.9 for p in prices])                  # Output: [90.0, 225.0, 36.0]
print(list(map(lambda p: p * 0.9, prices)))       # Output: [90.0, 225.0, 36.0]
print(sum(p for p in prices if p > 50))           # Output: 350.0

Use a comprehension when you are building one collection from another. Use a plain loop when there is real logic, several statements, or anything you would want to name. Reach for map and filter last — with a lambda they are strictly longer than the comprehension.

And use a generator expression when the result is consumed immediately, as on the third line, so nothing is built just to be counted.

Use the standard library

from collections import Counter
from pathlib import Path

kinds = ["Deposit", "Withdrawal", "Deposit"]

print(Counter(kinds).most_common(1))    # Output: [('Deposit', 2)]
print(Path("a") / "b" / "c.txt")        # Output: a/b/c.txt

Most "how do I count occurrences" code is Counter, most path manipulation is pathlib, most grouping is defaultdict, and most CSV parsing is csv. Before writing a helper, check whether collections, itertools, functools or pathlib already has it. They usually do, and theirs is tested.

Functions that do one thing

The practical test is whether you can name it without using "and". load_and_validate_and_save is three functions. A function you can describe in one sentence is one you can test, reuse and read.

Return early rather than nesting — the guard-clause pattern from Conditional Statements keeps the happy path at the left margin. And keep functions honest about side effects: a function called get_balance should not write to a file.

Comments explain why

balance = 100.0

# Bad: says what the code already says
balance = balance - 2.50      # subtract 2.50 from balance

# Good: says why
balance = balance - 2.50      # monthly fee, waived above $5,000 (see policy 4.2)

print(balance)     # Output: 95.0

The code is the authority on what. A comment restating it is one more thing to keep in sync, and it will go stale. Spend comments on the reason, the constraint, the thing you tried that did not work, and the bug the odd-looking line is avoiding.

A docstring is different — it is the function's public description, and it belongs on anything a caller has to understand.

The things that cost real time

  • Mutable default arguments. def f(items=[]) shares one list across every call. Use None. See Functions.
  • Bare except: swallows your own bugs and breaks Ctrl-C. Name the exception.
  • Floats for money. 0.1 + 0.2 != 0.3. Use Decimal, built from a string.
  • Mutating a list while iterating it. Build a new one.
  • Files without encoding="utf-8" — works on your machine, fails on someone else's.
  • Installing globally instead of in a virtual environment.

The tools worth setting up once

python3 -m venv .venv && source .venv/bin/activate

pip install ruff mypy pytest
pip freeze > requirements.txt

ruff check --fix .    # style and common bugs
mypy .                # type errors
pytest                # behaviour

Three tools, three different classes of problem, all runnable in one line each. Add them to CI and they run on every change without anyone remembering to.

Above all: write the boring version. The clever one-liner you are proud of is the line that confuses someone at 2am. Python's own guideline is that readability counts — type import this for the rest of it.

Next: Code Snippets.