Learn how to master variable scope in modular Python applications. This post explores best practices for managing scope in backend projects using FastAPI and SQLAlchemy.
When you work on a large Python codebase, especially in backend projects using Django, FastAPI, or Flask, you probably see the chaos that poor scope management can cause. From mysterious bugs and unpredictable state to namespace collisions and tangled dependencies, things get messy fast when variable scope isn’t handled with care.
In simple terms, scope is where a variable can be seen or used. For example:
def greet():
name = "Alice"
print(name)
print(name) # NameError: name is not defined
Here, name
is only visible inside the greet()
function. That’s its scope. Python uses something called the LEGB rule to decide how it looks for variables.
This rule stands for:
len
, print
, range
.When you reference a variable, Python starts at the innermost scope and moves outward until it finds it. Here’s a quick example:
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x)
inner()
outer() # prints "local"
If you remove x = "local"
from inner()
, Python prints "enclosing"
— and if that’s gone too, it prints "global"
. This rule is simple… until your app grows.
Let’s say you're building a backend service with FastAPI, and you start breaking your code into modules:
/project
├── main.py
├── database.py
├── models.py
├── routers/
│ └── user.py
If you don’t manage scope carefully, you’ll run into things like:
Let’s see how you can manage scope cleanly.
Your main.py
is your entry point. It should only:
os.environ
)Good:
# main.py
from fastapi import FastAPI
from routers import user
app = FastAPI()
app.include_router(user.router)
Bad:
# main.py
db_connection = connect_to_db()
SOME_MAGIC_GLOBAL_STATE = {}
# Used all over your app without structure
Why it’s bad: When you use global mutable objects, things can go wrong in concurrency, testing, or scaling. They also make your app harder to test.
Instead: Pass state as arguments or use dependency injection, FastAPI supports this.
Imagine you have a database.py
file that sets up your DB engine:
# database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine("sqlite:///example.db")
SessionLocal = sessionmaker(bind=engine)
This is good use of module scope. When you import SessionLocal
, it’s consistent and controlled.
Example:
# routers/user.py
from fastapi import Depends
from sqlalchemy.orm import Session
from database import SessionLocal
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@router.get("/users/")
def read_users(db: Session = Depends(get_db)):
return db.query(User).all()
Notice how we don’t expose too much. We don’t let engine
float around everywhere. SessionLocal
is the scoped, reusable object.
A common mistake:
# models.py
from database import SessionLocal
SessionLocal().execute("DROP TABLE users;") # this runs on import
Importing a module should not perform dangerous actions. That’s a scope + timing issue.
Instead:
Let’s get into deeper waters.
FastAPI lets you use function-level scope to inject services:
def get_current_user(token: str = Depends(oauth2_scheme)):
user = decode_token(token)
return user
This is better than making current_user
a global variable. It’s safer, more testable, and better scoped.
Sometimes, you need state. Don’t abuse globals, use classes:
# services/user_service.py
class UserService:
def __init__(self, db):
self.db = db
def get_user(self, user_id):
return self.db.query(User).filter_by(id=user_id).first()
In your endpoint:
@router.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
service = UserService(db)
return service.get_user(user_id)
Here, db
is passed down cleanly, no surprises, no globals.
Sometimes closures help with scope:
def make_greeting(prefix):
def greet(name):
return f"{prefix}, {name}!"
return greet
hello = make_greeting("Hello")
print(hello("Alice")) # Hello, Alice!
Use this in backends to build things like custom validators, filters, or pipelines with stored context.
To wrap it up, good scope management makes your Python code:
Here’s a quick cheat sheet:
Do This | Avoid This |
---|---|
Use local variables inside funcs | Using global variables as shared state |
Pass arguments explicitly | Relying on outer scope invisibly |
Encapsulate state with classes | Spreading config across files |
Keep module top-level clean | Running side effects on import |