Let’s kick off 2025 by writing some clean code together
When you’re deep in rapid prototyping, it’s tempting to skip clean scoping or reuse common variable names (hello, df!), thinking it will save time. But this can lead to sneaky bugs that break your workflow.
The good news? Writing clean, well-scoped code does not require additional effort once you understand the basic principles.
Let’s break it down.
What is variable scope?
Think of a variable as a container that will store some information. Scope refers to the region of your code where a variable is accessible.
Scope prevents accidental changes by limiting where variables can be read or modified. If every variable was accessible from anywhere, you’ll have to keep track of all of them to avoid overwriting it accidentally.
In Python, scope is defined by the LEGB rule, which stands for: local, enclosing, global and built-in.
Scoping in Python: LEGB rule
Let’s illustrate this with an example.
# Global scope, 7% tax
default_tax = 0.07
def calculate_invoice(price):
# Enclosing scope
discount = 0.10
total_after_discount = 0
def apply_discount():
nonlocal total_after_discount
# Local scope
tax = price * default_tax
total_after_discount = price - (price * discount)
return total_after_discount + tax
final_price = apply_discount()
return final_price, total_after_discount
# Built-in scope
print("Invoice total:", round(calculate_invoice(100)[0], 2))
1. Local scope
Variables inside a function are in the local scope. They can only be accessed within that function.
In the example, tax is a local variable inside apply_discount. It is not accessible outside this function.
2. Enclosing scope
These refer to variables in a function that contains a nested function. These variables are not global but can be accessed by the inner (nested) function. In this example, discount and total_after_discount are variables in the enclosing scope of apply_discount .
The nonlocal keyword:
The nonlocal keyword is used to modify variables in the enclosing scope, not just read them.
For example, suppose you want to update the variable total_after_discount, which is in the enclosing scope of the function. Without nonlocal, if you assign to total_after_discount inside the inner function, Python will treat it as a new local variable, effectively shadowing the outer variable. This can introduce bugs and unexpected behavior.
3. Global scope
Variables that are defined outside all functions and accessible throughout.
The global statement
When you declare a variable as global inside a function, Python treats it as a reference to the variable outside the function. This means that changes to it will affect the variable in the global scope.
With the global keyword, Python will create a new local variable.
x = 10 # Global variable
def modify_global():
global x # Declare that x refers to the global variable
x = 20 # Modify the global variable
modify_global()
print(x) # Output: 20. If "global" was not declared, this would read 10
4. Built-in scope
Refers to the reserved keywords that Python uses for it’s built-in functions, such as print , def , round and so on. This can be accessed at any level.
Key concept: global vs nonlocal keywords
Both keywords are crucial for modifying variables in different scopes, but they’re used differently.
- global: Used to modify variables in the global scope.
- nonlocal: Used to modify variables in the enclosing (non-global) scope.
Variable shadowing
Variable shadowing happens when a variable in an inner scope hides a variable from an outer scope.
Within the inner scope, all references to the variable will point to the inner variable, not the outer one. This can lead to confusion and unexpected outputs if you’re not careful.
Once execution returns to the outer scope, the inner variable ceases to exist, and any reference to the variable will point back to the outer scope variable.
Here’s a quick example. x is shadowed in each scope, resulting in different outputs depending on the context.
#global scope
x = 10
def outer_function():
#enclosing scope
x = 20
def inner_function():
#local scope
x = 30
print(x) # Outputs 30
inner_function()
print(x) # Outputs 20
outer_function()
print(x) # Outputs 10
Parameter shadowing
A similar concept to variable shadowing, but this occurs when a local variable redefines or overwrites a parameter passed to a function.
def foo(x):
x = 5 # Shadows the parameter `x`
return x
foo(10) # Output: 5
x is passed as 10. But it is immediately shadowed and overwritten by x=5
Scoping in recursive functions
Each recursive call gets its own execution context, meaning that the local variables and parameters in that call are independent of previous calls.
However, if a variable is modified globally or passed down explicitly as a parameter, the change can influence subsequent recursive calls.
- Local variables: These are defined inside the function and only affect the current recursion level. They do not persist between calls.
- Parameters passed explicitly to the next recursive call retain their values from the previous call, allowing the recursion to accumulate state across levels.
- Global variables: These are shared across all recursion levels. If modified, the change will be visible to all levels of recursion.
Let’s illustrate this with an example.
Example 1: Using a global variable (not recommended)
counter = 0 # Global variable
def count_up(n):
global counter
if n > 0:
counter += 1
count_up(n - 1)
count_up(5)
print(counter) # Output: 5
counter is a global variable shared across all recursive calls. It gets incremented at each level of recursion, and its final value (5) is printed after the recursion completes.
Example 2: Using parameters (recommended)
def count_up(n, counter=0):
if n > 0:
counter += 1
return count_up(n - 1, counter)
return counter
result = count_up(5)
print(result) # Output: 5
- counter is now a parameter of the function.
- counter is passed from one recursion level to the next, with it’s value updated at each level. The counter is not reinitialised in each call, rather, it’s current state is passed forward to the next recursion level.
- The function is now pure — there are no side effects and it only operates within it’s own scope.
- As the recursive function returns, the counter “bubbles up” to the top level and is returned at the base case.
Best practices
1. Use descriptive variable names
Avoid vague names like df or x. Use descriptive names such as customer_sales_df or sales_records_df for clarity.
2. Use capital letters for constants
This is the standard naming convention for constants in Python. For example, MAX_RETRIES = 5.
3. Avoid global variables as much as possible
Global variables introduces bugs and makes code harder to test and maintain. It’s best to pass variables explicitly between functions.
4. Aim to write pure functions where possible
What’s a pure function?
- Deterministic: It always produces the same output for the same input. It’s not affected by external states or randomness.
- Side-effect-free: It does not modify any external variables or states. It operates solely within its local scope.
Using nonlocal or global would make the function impure.
However, if you’re working with a closure, you should use the nonlocal keyword to modify variables in the enclosing (outer) scope, which helps prevent variable shadowing.
A closure occurs when a nested function (inner function) captures and refers to variables from its enclosing function (outer function). This allows the inner function to “remember” the environment in which it was created, including access to variables from the outer function’s scope, even after the outer function has finished executing.
The concept of closures can go really deep, so tell me in the comments if this is something I should dive into in the next article! 🙂
5. Avoid variable shadowing and parameter shadowing
If you need to refer to an outer variable, avoid reusing its name in an inner scope. Use distinct names to clearly distinguish the variables.
Wrapping up
That’s a wrap! Thanks for sticking with me till the end.
Have you encountered any of these challenges in your own work? Drop your thoughts in the comments below!
I write regularly on Python, software development and the projects I build, so give me a follow to not miss out. See you in the next article 🙂
Why Variable Scoping Can Make or Break Your Data Science Workflow was originally published in Towards Data Science on Medium, where people are continuing the conversation by highlighting and responding to this story.
Originally appeared here:
Why Variable Scoping Can Make or Break Your Data Science Workflow
Go Here to Read this Fast! Why Variable Scoping Can Make or Break Your Data Science Workflow