Python Best Practices Every Developer Should Know

Python's philosophy is built around readability and simplicity. The language itself pushes you toward clean code, but knowing the community conventions is what separates good Python from great Python. This guide covers the practices that experienced Python developers follow by default.

1. Always Use Virtual Environments

Never install packages globally. Use a virtual environment to isolate dependencies per project.

# Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate        # macOS/Linux
.venv\Scripts\activate           # Windows

# Install dependencies
pip install -r requirements.txt

Use pyproject.toml with tools like Poetry or PDM for modern dependency management:

poetry init
poetry add requests
poetry add --group dev pytest

Why it matters: Global package installs cause version conflicts across projects. Virtual environments keep your projects reproducible and portable.

2. Follow PEP 8, and Enforce It Automatically

PEP 8 is the official Python style guide. Rather than memorising every rule, automate it:

pip install ruff   # fast all-in-one linter + formatter
ruff check .
ruff format .

Key rules to internalise:

  • 4 spaces for indentation, never tabs
  • Snake case for variables and functions: user_name, fetch_data
  • Pascal case for classes: UserProfile, DataProcessor
  • UPPER_SNAKE_CASE for constants: MAX_RETRIES = 3
  • Two blank lines between top-level definitions
  • Lines no longer than 88–100 characters (the community has moved past PEP 8's original 79)

3. Use Type Hints

Type hints (introduced in Python 3.5) make your code self-documenting and enable static analysis with tools like mypy and pyright.

from typing import Optional

def get_user(user_id: int) -> Optional[dict]:
    ...

def process_items(items: list[str]) -> list[str]:
    return [item.strip() for item in items if item]

For complex types, use TypedDict or dataclasses:

from dataclasses import dataclass, field

@dataclass
class Article:
    id: int
    title: str
    tags: list[str] = field(default_factory=list)
    is_published: bool = False

Avoid Any unless truly necessary. It defeats the purpose of type annotations.

4. Write Pythonic Code with Comprehensions and Generators

Prefer list comprehensions over verbose loops when the intent is clear:

# Less Pythonic
squares = []
for n in range(10):
    squares.append(n ** 2)

# Pythonic
squares = [n ** 2 for n in range(10)]

# With a filter condition
evens = [n for n in range(20) if n % 2 == 0]

For large datasets, use generators to avoid loading everything into memory:

def read_large_file(filepath: str):
    with open(filepath) as f:
        for line in f:
            yield line.strip()

# Memory-efficient processing
for line in read_large_file("huge_log.txt"):
    process(line)

Use dict comprehensions and set comprehensions the same way:

word_lengths = {word: len(word) for word in ["python", "django", "flask"]}
unique_tags   = {tag.lower() for tag in raw_tags}

5. Use Context Managers for Resource Management

Always use with statements for files, database connections, and locks. It guarantees cleanup even if an exception occurs.

# Reading a file
with open("data.json", "r") as f:
    data = json.load(f)

# Database session (SQLAlchemy)
with db.session() as session:
    user = session.get(User, user_id)

Create your own context managers using contextlib.contextmanager:

from contextlib import contextmanager
import time

@contextmanager
def timer(label: str):
    start = time.perf_counter()
    try:
        yield
    finally:
        elapsed = time.perf_counter() - start
        print(f"{label}: {elapsed:.3f}s")

with timer("data processing"):
    process_large_dataset()

6. Handle Exceptions Specifically. Never Silence Them

Catch the narrowest exception type you can. Broad except Exception clauses hide real bugs.

# Bad — hides all errors
try:
    result = fetch_data(url)
except Exception:
    pass

# Good — handle what you expect
try:
    result = fetch_data(url)
except requests.Timeout:
    logger.warning("Request timed out, retrying...")
    result = fetch_data(url, timeout=30)
except requests.HTTPError as e:
    logger.error("HTTP error: %s", e.response.status_code)
    raise

Use custom exception classes for domain-specific errors:

class ArticleNotFoundError(ValueError):
    def __init__(self, article_id: int):
        super().__init__(f"Article {article_id} not found")
        self.article_id = article_id

7. Prefer pathlib Over os.path

pathlib.Path is object-oriented, cross-platform, and far more readable than os.path string juggling:

from pathlib import Path

# Building paths
base = Path("data")
config_file = base / "config" / "settings.json"

# Reading / writing
text = config_file.read_text(encoding="utf-8")
config_file.write_text(json.dumps(config), encoding="utf-8")

# Checking existence
if config_file.exists():
    ...

# Iterating files
for md_file in Path("content").glob("**/*.md"):
    print(md_file.name)

8. Write Tests with pytest

pytest is the community standard for Python testing. It needs no boilerplate and its assertion introspection gives clear failure messages.

pip install pytest pytest-cov
# tests/test_articles.py
import pytest
from myapp.articles import get_slug

def test_slug_converts_spaces():
    assert get_slug("Hello World") == "hello-world"

def test_slug_handles_empty():
    assert get_slug("") == ""

@pytest.mark.parametrize("title,expected", [
    ("Python Tips", "python-tips"),
    ("  leading spaces  ", "leading-spaces"),
])
def test_slug_parametrized(title, expected):
    assert get_slug(title) == expected

Run with coverage:

pytest --cov=myapp --cov-report=term-missing

9. Use __slots__ for Memory-Efficient Classes

When creating many instances of a class, __slots__ prevents the creation of a per-instance __dict__, reducing memory usage significantly:

class Point:
    __slots__ = ("x", "y")

    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

10. Structure Your Project Properly

A well-structured Python project makes onboarding and scaling easier:

my-project/
├── src/
│   └── myapp/
│       ├── __init__.py
│       ├── models.py
│       ├── services.py
│       └── utils.py
├── tests/
│   ├── conftest.py
│   └── test_services.py
├── pyproject.toml
├── .ruff.toml
├── .env.example
└── README.md

Use src/ layout to prevent accidental imports from the project root during development.

Conclusion

Pythonic code is not just about syntax. It is about intent. Use virtual environments to stay isolated, type hints to stay clear, comprehensions to stay concise, and pytest to stay confident. These practices are not opinions; they are the accumulated wisdom of the Python community encoded in PEPs, tooling defaults, and decades of production experience.


References: