Skip to content

Code Quality

Best practices for writing correct, maintainable, and well-styled Python code.

Mental Model

Good Python code makes the object model work for you rather than against you. Use immutable defaults, compare with == instead of is, and let Python's built-in idioms (context managers, unpacking, comprehensions) express intent clearly. Correctness comes from respecting how names, scopes, and mutability actually behave.

Correctness

Avoid Mutable Default Arguments

```python

WRONG: Default list is shared between calls

def add_item(item, lst=[]): lst.append(item) return lst

RIGHT: Use None as default

def add_item(item, lst=None): if lst is None: lst = [] lst.append(item) return lst ```

Fix Late Binding in Closures

```python

WRONG: All functions return the last value of i

funcs = [lambda: i for i in range(3)]

RIGHT: Capture i as default argument

funcs = [lambda x=i: x for i in range(3)] ```

Use is Correctly

```python

WRONG: Using is for value comparison

if x is 1000: # May fail! pass

RIGHT: Use == for values

if x == 1000: pass

RIGHT: Use is for None, True, False

if result is None: pass ```

Handle Exceptions Properly

```python

WRONG: Bare except catches everything

try: risky_operation() except: pass

RIGHT: Catch specific exceptions

try: risky_operation() except ValueError as e: logger.error(f"Invalid value: {e}") except (TypeError, KeyError) as e: logger.error(f"Type or key error: {e}") ```

Write Tests

```python import unittest

def add(a, b): return a + b

class TestAdd(unittest.TestCase): def test_positive_numbers(self): self.assertEqual(add(2, 3), 5)

def test_negative_numbers(self):
    self.assertEqual(add(-1, -1), -2)

def test_mixed_numbers(self):
    self.assertEqual(add(-1, 1), 0)

if name == 'main': unittest.main() ```


Maintainability

Use Descriptive Names

```python

BAD: Cryptic names

def p(d, n): return d * (1 + r) ** n

GOOD: Clear, descriptive names

def calculate_compound_interest(principal, years, rate=0.05): return principal * (1 + rate) ** years

BAD: Single-letter variables

n = len(users)

GOOD: Meaningful names

user_count = len(users) active_user_count = len([u for u in users if u.is_active]) ```

Keep Functions Small

```python

BAD: One function doing too much

def process_user_data(user): # Validate if not user.email: raise ValueError("No email") # Transform user.name = user.name.title() # Save db.save(user) # Notify send_email(user.email, "Welcome!")

GOOD: Single responsibility

def validate_user(user): if not user.email: raise ValueError("No email")

def normalize_user(user): user.name = user.name.title()

def save_user(user): db.save(user)

def notify_user(user): send_email(user.email, "Welcome!")

def process_user(user): validate_user(user) normalize_user(user) save_user(user) notify_user(user) ```

Write Docstrings

```python def calculate_discount(price, discount_percent): """ Calculate the discounted price.

Args:
    price: Original price in dollars.
    discount_percent: Discount percentage (0-100).

Returns:
    The discounted price.

Raises:
    ValueError: If discount_percent is not between 0 and 100.

Example:
    >>> calculate_discount(100, 20)
    80.0
"""
if not 0 <= discount_percent <= 100:
    raise ValueError("Discount must be between 0 and 100")
return price * (1 - discount_percent / 100)

```

Use Type Hints

```python from typing import List, Optional, Dict

def find_user(user_id: int) -> Optional[Dict[str, str]]: """Find user by ID, return None if not found.""" pass

def get_active_users(users: List[Dict]) -> List[Dict]: """Filter and return only active users.""" return [u for u in users if u.get('active')] ```


Style Guide (PEP 8)

Naming Conventions

```python

Variables and functions: snake_case

user_name = "Alice" def calculate_total(): pass

Classes: PascalCase

class UserAccount: pass

Constants: UPPER_SNAKE_CASE

MAX_CONNECTIONS = 100 DEFAULT_TIMEOUT = 30

Private: leading underscore

_internal_cache = {} def _helper_function(): pass

"Private" (name mangling): double underscore

class MyClass: def init(self): self.__private_attr = 42 ```

Spacing and Formatting

```python

Spaces around operators

x = 1 + 2 y = x * 3

No space before colon in slices

items[1:3] items[::2]

Spaces after commas

func(a, b, c) data = [1, 2, 3]

Two blank lines between top-level definitions

def function_one(): pass

def function_two(): pass

class MyClass: pass ```

Import Organization

```python

Standard library imports

import os import sys from collections import defaultdict

Third-party imports

import numpy as np import pandas as pd

Local imports

from mypackage import mymodule from mypackage.utils import helper ```

Line Length

```python

Keep lines under 79-88 characters

Break long lines appropriately

Long function call

result = some_function( argument_one, argument_two, argument_three, )

Long string

message = ( "This is a very long message that needs to be " "split across multiple lines for readability." )

Long condition

if (condition_one and condition_two and condition_three): do_something() ```


Code Review Checklist

Correctness

  • [ ] No mutable default arguments
  • [ ] Closures capture variables correctly
  • [ ] Exceptions handled appropriately
  • [ ] Edge cases considered
  • [ ] Tests written and passing

Maintainability

  • [ ] Functions are small and focused
  • [ ] Names are descriptive
  • [ ] Docstrings present for public APIs
  • [ ] Type hints used where helpful
  • [ ] No code duplication

Style

  • [ ] Follows PEP 8 conventions
  • [ ] Imports organized properly
  • [ ] Consistent formatting
  • [ ] Line length reasonable
  • [ ] Comments explain "why", not "what"

Summary

Aspect Key Points
Correctness Avoid mutable defaults, fix late binding, handle exceptions
Maintainability Descriptive names, small functions, good documentation
Style Follow PEP 8, consistent formatting, organized imports

Key principles:

  • Code is read more than written--optimize for readability
  • Test your code thoroughly
  • Follow established conventions
  • Keep functions focused and small
  • Document public APIs

Exercises

Exercise 1. Mutable default arguments are a common source of bugs. Predict the output:

```python def add_item(item, lst=[]): lst.append(item) return lst

r1 = add_item("a") r2 = add_item("b") r3 = add_item("c", [])

print(r1) print(r2) print(r3) print(r1 is r2) ```

Why does r2 contain both "a" and "b"? Why is r1 is r2 True? Write the corrected version of this function.

Solution to Exercise 1

Output:

text ['a', 'b'] ['a', 'b'] ['c'] True

The default argument lst=[] is evaluated once at function definition time, not on each call. Every call that uses the default shares the same list object. add_item("a") appends to this shared list, and add_item("b") appends to the same list. r1 and r2 are the same object (r1 is r2 is True).

r3 gets a fresh list because [] was passed explicitly, bypassing the default.

Corrected version:

python def add_item(item, lst=None): if lst is None: lst = [] lst.append(item) return lst

Using None as a sentinel and creating a new list inside the function body ensures each call gets its own list.


Exercise 2. Late binding in closures created inside loops is a classic Python pitfall. Predict the output:

```python funcs = [lambda: i for i in range(4)] print([f() for f in funcs])

funcs2 = [lambda i=i: i for i in range(4)] print([f() for f in funcs2]) ```

Why do all functions in funcs return 3? How does the default argument trick in funcs2 fix the problem? What is the underlying mechanism?

Solution to Exercise 2

Output:

text [3, 3, 3, 3] [0, 1, 2, 3]

All lambdas in funcs capture the variable i by reference, not its value. By the time any lambda is called, the loop has finished and i is 3. All four functions look up the same i and find 3.

lambda i=i: i fixes this by capturing the current value of i as a default argument. Default arguments are evaluated at function definition time (during each loop iteration), so each lambda gets a snapshot: i=0, i=1, i=2, i=3.

The underlying mechanism: closures hold references to cell objects (shared mutable containers), while default arguments store values directly in the function object's __defaults__ tuple.


Exercise 3. Exception handling has subtleties around variable scope. Predict the output:

```python try: x = 1 / 0 except ZeroDivisionError as e: error = e print(type(error))

try: print(e) except NameError: print("e is gone")

print(error) ```

Why is e deleted after the except block but error survives? What Python design decision explains this behavior?

Solution to Exercise 3

Output:

text <class 'ZeroDivisionError'> e is gone division by zero

Python deletes the exception variable e at the end of the except block. This is by design (PEP 3110): exception objects hold references to the traceback, which holds references to all local variables in the stack frames. If e survived, it would create a reference cycle preventing garbage collection of those frames.

However, error = e creates a separate binding to the same exception object. Since error is a normal variable (not the as target), Python does not delete it. The exception object itself survives as long as error references it.

This is equivalent to Python implicitly running del e at the end of the except block.