PythonBest Practices

Best Practices

Writing Python that merely runs is easy. Writing Python that stays readable, debuggable, and safe to change six months from now takes discipline. This page pulls together the habits that consistently separate maintainable codebases from fragile ones — treat it as a practical checklist to run your own code against.

1. Follow PEP 8

PEP 8 is Python's official style guide — 4-space indentation, snake_case for functions and variables, PascalCase for classes, sensible line lengths, consistent whitespace. Consistency matters more than any individual rule: it means anyone (including future you) can read unfamiliar code without friction. Tools like black and ruff format enforce this automatically so it stops being a manual chore.

2. Write docstrings

Python
def calculate_discount(price: float, percent: float) -> float:
    """Return the price after applying a percentage discount.

    Args:
        price: Original price, must be non-negative.
        percent: Discount percentage between 0 and 100.

    Returns:
        The discounted price.
    """
    return price * (1 - percent / 100)

A good docstring explains why and what, not a restatement of the code. It also powers help(), IDE tooltips, and auto-generated documentation for free.

3. Use type hints

Python
def find_user(user_id: int) -> dict[str, str] | None:
    ...


from dataclasses import dataclass


@dataclass
class Point:
    x: float
    y: float

Type hints don't change runtime behavior, but they let tools like mypy and your editor catch entire categories of bugs — passing a str where an int is expected — before the code ever runs.

4. Prefer composition over deep inheritance

Python
# Fragile: deep inheritance chains couple unrelated behavior together
class Animal: ...
class Swimmer(Animal): ...
class Flyer(Swimmer): ...  # a Duck needs both, forcing an awkward chain

# Sturdier: compose small, focused pieces
class Duck:
    def __init__(self):
        self.swim_behavior = SwimBehavior()
        self.fly_behavior = FlyBehavior()

Deep inheritance hierarchies tend to become rigid and hard to change safely. Favoring composition — building objects out of smaller, swappable pieces — keeps each piece easy to test and reuse independently.

5. Use context managers for resources

Python
# Risky: file stays open if an exception is raised before close()
f = open('data.txt')
data = f.read()
f.close()

# Safe: the file is always closed, even on error
with open('data.txt') as f:
    data = f.read()

Files, database connections, locks, and network sockets should almost always be managed with with blocks so cleanup happens automatically, even when something goes wrong halfway through.

6. Avoid mutable default arguments

Python
# Bug: the same list is reused across every call!
def add_item(item, items=[]):
    items.append(item)
    return items

# Fix: use None as the sentinel, create a fresh list each call
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items
Warning
Default argument values are evaluated exactly once, when the function is defined — not on every call. A mutable default like `[]` or `` is silently shared and mutated across every call site, producing some of the most confusing bugs in Python.
7. Use virtual environments, always

Bash
python -m venv .venv
source .venv/bin/activate    # macOS/Linux
.venv\Scripts\activate       # Windows

pip install -r requirements.txt

Every project should have its own isolated set of dependencies. Installing packages globally leads to version conflicts between projects and makes it impossible to reliably reproduce someone else's environment.

8. Write tests

Python
# test_discount.py
from discount import calculate_discount


def test_calculate_discount_applies_percentage():
    assert calculate_discount(100, 20) == 80


def test_calculate_discount_zero_percent():
    assert calculate_discount(50, 0) == 50

Tests (with pytest or the built-in unittest) turn "I think this still works" into "I know this still works." They're what make refactoring and upgrading dependencies safe instead of terrifying.

9. Use pathlib over raw string paths

Python
# Old style: string concatenation, platform-specific separators
path = base_dir + "/" + "data" + "/" + "file.csv"

# pathlib: readable, cross-platform, object-oriented
from pathlib import Path

path = Path(base_dir) / "data" / "file.csv"
if path.exists():
    contents = path.read_text()

pathlib handles path separators correctly across operating systems and bundles common filesystem operations (.exists(), .read_text(), .mkdir()) directly onto the path object.

10. Prefer f-strings

Python
name = "Ada"
score = 97.5

# Old styles
"Hello, %s! Score: %.1f" % (name, score)
"Hello, {}! Score: {:.1f}".format(name, score)

# f-strings: clearer, and evaluated fastest
f"Hello, {name}! Score: {score:.1f}"
11. Use logging, not print

Python
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

logger.info("Starting job for user %s", user_id)
logger.warning("Retrying after transient error")
logger.error("Job failed", exc_info=True)

print() is fine for a quick throwaway script, but real applications need severity levels, timestamps, the ability to turn noisy output off in production, and a way to route messages to files or external log aggregators — all of which logging provides and print() does not.

The checklist
  1. Follow PEP 8 (or let black/ruff format do it for you).

  2. Write docstrings for public functions, classes, and modules.

  3. Add type hints and check them with mypy or your editor.

  4. Prefer composition over deep inheritance chains.

  5. Use with blocks for files, connections, and locks.

  6. Never use a mutable object ([], {}) as a default argument.

  7. Always develop inside a virtual environment.

  8. Write tests for the logic you care about, and run them before merging.

  9. Use pathlib instead of manual string path manipulation.

  10. Prefer f-strings for formatting text.

  11. Use the logging module instead of print() for anything beyond a quick script.

Tip
None of these rules are absolute — a five-line throwaway script doesn't need a test suite or type hints. The value of a checklist like this is knowing which rule to break intentionally, rather than skipping it by accident.

Good Python habits compound: consistent style, clear types, safe resource handling, and real tests all make a codebase easier to hand off, easier to debug at 2am, and easier to trust as it grows.