Python Basics — Python Best Practices

5 minute read

Python Best Practices

Writing code that works is the first step. Writing code that’s readable, maintainable, and Pythonic is the mark of a professional.

PEP 8 Summary

PEP 8 is Python’s official style guide. Here are the most important rules:

Naming Conventions

# Variables and functions: snake_case
user_name = "Alice"
def calculate_total():
    pass

# Classes: PascalCase
class ShoppingCart:
    pass

# Constants: UPPER_SNAKE_CASE
MAX_RETRIES = 3
DATABASE_URL = "localhost"

# Private: leading underscore
_internal_cache = {}
def _helper_function():
    pass

# "Really private": double underscore (name mangling)
class MyClass:
    __secret = 42

Indentation and Spacing

# ✅ 4 spaces per indentation level (never tabs)
if condition:
    do_something()

# ✅ Spaces around operators
x = 5 + 3
result = x * 2

# ✅ Space after comma, not before
items = [1, 2, 3]
func(arg1, arg2)

# ✅ No space inside brackets
items[0]       # not items[ 0 ]
func(arg)      # not func( arg )

# ✅ Blank lines
# 2 blank lines before top-level functions/classes
# 1 blank line between methods in a class

Import Order (PEP 8)

# 1. Standard library
import os
import sys
from datetime import datetime

# 2. Third-party packages
import requests
from flask import Flask

# 3. Local/project imports
from myapp.models import User
from myapp.utils import validate

Line Length

# ✅ Max 79 characters for code, 72 for docstrings

# ✅ Break long lines with parentheses
result = (
    first_value
    + second_value
    - third_value
)

# ✅ Or with backslash (less preferred)
total = first_value + \
        second_value + \
        third_value

Pythonic Code Patterns

Use Truthiness

# ❌ Verbose
if len(items) > 0:
    process(items)

if name != "":
    greet(name)

if result is not None:
    use(result)

# ✅ Pythonic
if items:
    process(items)

if name:
    greet(name)

if result is not None:  # Keep this one — None check should be explicit
    use(result)

Use Unpacking

# ❌ Index access
point = (3, 4)
x = point[0]
y = point[1]

# ✅ Unpacking
x, y = (3, 4)

# ✅ Swap variables
a, b = b, a

# ✅ Ignore values with _
name, _, age = ("Alice", "unused", 30)
first, *rest = [1, 2, 3, 4, 5]  # first=1, rest=[2,3,4,5]

Use enumerate() and zip()

# ❌ Manual index
for i in range(len(items)):
    print(i, items[i])

# ✅ enumerate
for i, item in enumerate(items):
    print(i, item)

# ❌ Parallel iteration with index
for i in range(len(names)):
    print(names[i], scores[i])

# ✅ zip
for name, score in zip(names, scores):
    print(name, score)

Use Comprehensions

# ❌ Manual loop
squares = []
for x in range(10):
    squares.append(x ** 2)

# ✅ List comprehension
squares = [x ** 2 for x in range(10)]

# ✅ Dict comprehension
word_lengths = {w: len(w) for w in words}

# ✅ Set comprehension
unique_lengths = {len(w) for w in words}

Use f-Strings

name = "Alice"
age = 30

# ❌ Old style
print("Hello, %s! You are %d." % (name, age))
print("Hello, {}! You are {}.".format(name, age))

# ✅ f-strings (Python 3.6+)
print(f"Hello, {name}! You are {age}.")
print(f"Debug: {name=}, {age=}")  # Python 3.8+

EAFP vs LBYL

Two approaches to handling potential problems:

LBYL — “Look Before You Leap”

# Check first, then act
if "key" in dictionary:
    value = dictionary["key"]
else:
    value = default

if os.path.exists(filename):
    with open(filename) as f:
        data = f.read()

EAFP — “Easier to Ask Forgiveness than Permission”

# Try it, handle failure
try:
    value = dictionary["key"]
except KeyError:
    value = default

try:
    with open(filename) as f:
        data = f.read()
except FileNotFoundError:
    data = ""

Python favors EAFP — it’s often faster (one operation instead of two) and avoids race conditions (file could be deleted between check and open).

But for simple checks, LBYL with .get() is cleaner:

# ✅ Best of both worlds
value = dictionary.get("key", default)

Writing Readable Code

Meaningful Names

# ❌ Cryptic
def p(l, n):
    return [x for x in l if x > n]

# ✅ Clear
def filter_above_threshold(values, threshold):
    return [v for v in values if v > threshold]

Small, Focused Functions

# ❌ One giant function doing everything
def process_user_data(filepath):
    # ... 200 lines of code ...
    pass

# ✅ Small, composable functions
def load_user_data(filepath):
    """Load user data from CSV file."""
    ...

def validate_users(users):
    """Validate user records, return valid ones."""
    ...

def generate_report(users):
    """Generate summary report from user data."""
    ...

# Clear pipeline
data = load_user_data("users.csv")
valid_users = validate_users(data)
report = generate_report(valid_users)

Use Docstrings

def calculate_bmi(weight_kg, height_m):
    """
    Calculate Body Mass Index (BMI).
    
    Args:
        weight_kg: Weight in kilograms.
        height_m: Height in meters.
    
    Returns:
        BMI as a float, rounded to 1 decimal place.
    
    Raises:
        ValueError: If weight or height is non-positive.
    """
    if weight_kg <= 0 or height_m <= 0:
        raise ValueError("Weight and height must be positive")
    return round(weight_kg / (height_m ** 2), 1)

Code Organization Tips

Project Structure

my_project/
├── README.md
├── requirements.txt
├── main.py
├── config.py
├── utils/
│   ├── __init__.py
│   ├── helpers.py
│   └── validators.py
├── models/
│   ├── __init__.py
│   └── user.py
└── tests/
    ├── __init__.py
    ├── test_helpers.py
    └── test_user.py

Separation of Concerns

# ❌ Mixed concerns
def save_user(name, age):
    if not name:
        print("Error: name required")  # UI concern
        return
    conn = sqlite3.connect("app.db")   # DB concern
    conn.execute("INSERT ...", (name, age))
    conn.commit()
    print(f"Saved {name}")             # UI concern

# ✅ Separated
def validate_user(name, age):
    """Pure validation logic."""
    if not name:
        raise ValueError("Name is required")
    if age < 0:
        raise ValueError("Age must be non-negative")

def save_to_db(name, age, db_path="app.db"):
    """Database persistence."""
    conn = sqlite3.connect(db_path)
    conn.execute("INSERT ...", (name, age))
    conn.commit()

Version Control Basics

Start every project with git:

# Initialize a new repository
git init

# Add files
git add .

# Commit
git commit -m "Initial commit"

# Common workflow
git add modified_file.py
git commit -m "Add user validation function"

# Check status
git status
git log --oneline

Write Good Commit Messages

✅ "Add email validation to user registration"
✅ "Fix off-by-one error in pagination"
✅ "Refactor database connection to use context manager"

❌ "Fixed stuff"
❌ "Update"
❌ "asdfgh"

Summary

Practice Why
Follow PEP 8 Consistent, readable code
Use f-strings Clean string formatting
Prefer EAFP Pythonic error handling
Write docstrings Document intent and usage
Small functions Easier to test and reuse
Use comprehensions Concise, expressive code
Use with for files Automatic resource cleanup
Use git Track changes, collaborate

Next up: Next Steps →

Updated: