Python Basics — Scope Best Practices

3 minute read

Scope Best Practices

Writing clean, well-scoped code makes your programs easier to read, test, and debug. Here are the essential practices every Python developer should follow.

1. Minimize Global State

Global variables are shared across your entire module. The more you have, the harder it is to track what’s changing and where.

# ❌ Too much global state
user_name = ""
user_age = 0
user_email = ""
is_logged_in = False

def login(name, age, email):
    global user_name, user_age, user_email, is_logged_in
    user_name = name
    user_age = age
    user_email = email
    is_logged_in = True

# ✅ Encapsulate in a dictionary or class
def create_user(name, age, email):
    return {
        "name": name,
        "age": age,
        "email": email,
        "logged_in": True
    }

user = create_user("Alice", 30, "alice@example.com")

2. Pass Data Explicitly as Arguments

Functions that receive all their inputs as arguments are easier to understand and test:

# ❌ Depends on global state — hidden dependency
tax_rate = 0.18

def calculate_total(price):
    return price + (price * tax_rate)   # Where does tax_rate come from?

# ✅ Explicit — everything the function needs is passed in
def calculate_total(price, tax_rate):
    return price + (price * tax_rate)

total = calculate_total(1000, 0.18)
print(total)  # 1180.0

3. Use Return Values Over Mutations

Pure functions — functions that take input and return output without changing external state — are the easiest to reason about:

# ❌ Mutates external data
results = []

def process_item(item):
    results.append(item.upper())   # side effect

for item in ["hello", "world"]:
    process_item(item)

# ✅ Returns new data
def process_items(items):
    return [item.upper() for item in items]

results = process_items(["hello", "world"])
print(results)  # ['HELLO', 'WORLD']

4. Constants — UPPER_CASE Naming Convention

Variables that shouldn’t change are called constants. Python doesn’t enforce immutability, but the naming convention signals intent:

# ✅ Constants in UPPER_CASE
MAX_RETRIES = 3
API_BASE_URL = "https://api.example.com"
DEFAULT_TIMEOUT = 30
PI = 3.14159

# Use them in functions
def make_request(endpoint, timeout=DEFAULT_TIMEOUT):
    for attempt in range(MAX_RETRIES):
        url = f"{API_BASE_URL}/{endpoint}"
        print(f"Attempt {attempt + 1}: {url} (timeout={timeout}s)")
        # ... make the request
        break

make_request("users")
# Attempt 1: https://api.example.com/users (timeout=30s)

5. Module-Level Scope Organization

Structure your Python files in a clear, predictable order:

"""Module docstring — what this module does."""

# 1. Imports (stdlib → third-party → local)
import os
import sys
from datetime import datetime

# 2. Constants
MAX_SIZE = 1024
DEFAULT_NAME = "unnamed"

# 3. Module-level functions
def helper():
    pass

def main_function():
    pass

# 4. Classes
class MyClass:
    pass

# 5. Entry point
if __name__ == "__main__":
    main_function()

6. Keep Functions Small and Focused

Small functions naturally have smaller scopes with fewer variables to track:

# ❌ One big function with many local variables
def process_order(order):
    # 50 lines of discount logic
    # 30 lines of tax calculation
    # 20 lines of shipping logic
    # 15 lines of notification logic
    pass

# ✅ Split into focused functions
def calculate_discount(order):
    """Calculate discount based on order value."""
    if order["total"] > 5000:
        return order["total"] * 0.10
    return 0

def calculate_tax(subtotal, tax_rate=0.18):
    """Calculate tax on the subtotal."""
    return subtotal * tax_rate

def calculate_shipping(weight, distance):
    """Calculate shipping cost."""
    return weight * 0.5 + distance * 0.1

def process_order(order):
    discount = calculate_discount(order)
    subtotal = order["total"] - discount
    tax = calculate_tax(subtotal)
    shipping = calculate_shipping(order["weight"], order["distance"])
    return subtotal + tax + shipping

7. Avoid Shadowing Built-ins

Never use Python built-in names as variable names:

# ❌ Shadows built-ins
list = [1, 2, 3]           # Now list() is broken!
dict = {"key": "value"}    # Now dict() is broken!
str = "hello"              # Now str() is broken!
input = "some text"        # Now input() is broken!

# ✅ Use descriptive names instead
numbers = [1, 2, 3]
config = {"key": "value"}
greeting = "hello"
user_input = "some text"

Quick Reference Checklist

Practice Do Don’t
Global state Minimize; use constants Scatter mutable globals everywhere
Function inputs Pass explicitly as arguments Rely on globals implicitly
Function outputs Return values Mutate external variables
Constants UPPER_CASE naming Magic numbers in code
Built-in names Use descriptive alternatives Shadow with list, str, dict
File organization Imports → Constants → Functions → Classes → Main Random order
Function size Small, focused, single purpose One giant function

Summary

  • Keep global state to a minimum — prefer passing data through arguments
  • Write pure functions that return values instead of mutating external state
  • Name constants in UPPER_CASE to signal they shouldn’t change
  • Organize modules in a consistent order: imports, constants, functions, classes, main
  • Never shadow built-in names like list, dict, str
  • Keep functions small and focused for naturally cleaner scopes

Next up: Scope & Closures — Practice Exercises →

Updated: