In Python, scope refers to the part of the program where a variable can be accessed or modified. Understanding scope is essential when writing code, especially when working with functions, because it affects how variables are created, accessed, and maintained.
- Local Scope:
- Variables defined within a function.
- Only accessible within that function.
- Nonlocal Scope:
- Variables defined in an enclosing function.
- Accessible in nested functions but not in the outermost scope.
- Global Scope:
- Variables defined outside of all functions.
- Accessible from anywhere within the file.
- Lexical scoping (or static scoping) means that the location of a variable’s assignment in the source code determines its scope.
- For example, a variable defined inside a function is local to that function, even if that function is called from another part of the program.
Variables defined within a function are local to that function.
def greet():
name = "Muhammad Hashim" # Local variable
print(f"Hello, {name}!")
greet() # Output: Hello, Muhammad Hashim
print(name) # Error: name is not defined
name
is a local variable defined inside thegreet()
function.- It is only accessible within the
greet()
function. Trying to access it outside the function will result in an error becausename
does not exist in the global scope.
Variables defined outside of all functions are global and can be accessed anywhere.
name = "Muhammad Hashim" # Global variable
def greet():
print(f"Hello, {name}!")
greet() # Output: Hello, Muhammad Hashim
print(name) # Output: Muhammad Hashim
- The
name
variable is defined outside any function, so it is global. - Both the
greet()
function and the main program can access it.
Even if a local and global variable have the same name, they are treated as different variables.
name = "Muhammad Hashim" # Global variable
def greet():
name = "Hashim" # Local variable
print(f"Hello, {name}!")
greet() # Output: Hello, Hashim
print(name) # Output: Muhammad Hashim
- The
name
insidegreet()
is local to the function and different from the globalname
. - When
greet()
is called, it prints "Hashim" (the localname
). Outside ofgreet()
, it prints "Muhammad Hashim" (the globalname
).
If you have nested functions, a variable defined in the enclosing function is nonlocal to the nested function.
def outer():
name = "Hashim" # Nonlocal variable
def inner():
nonlocal name
name = "Muhammad Hashim"
print(f"Inside inner: {name}")
inner()
print(f"Inside outer: {name}")
outer()
Inside inner: Muhammad Hashim
Inside outer: Muhammad Hashim
- The
name
defined insideouter()
is nonlocal because it is enclosing forinner()
. - By using
nonlocal
, theinner()
function can modify thename
variable fromouter()
. Withoutnonlocal
, a new local variable would have been created insideinner()
.
- Names assigned inside a
def
(function) can only be seen by the code within thatdef
. - Names assigned inside a
def
do not clash with variables outside thedef
, even if the same names are used elsewhere. - The scope of a variable is determined by where it is assigned in your code.
X = "Global X" # Global variable
def outer():
X = "Outer X" # Nonlocal variable
def inner():
nonlocal X # Refers to the nonlocal X in `outer`
X = "Inner X" # Local to inner, modifies outer's X
print(f"Inside inner: {X}")
inner()
print(f"Inside outer: {X}")
outer()
print(f"Outside all: {X}")
Inside inner: Inner X
Inside outer: Inner X
Outside all: Global X
- Global X is defined at the module level.
X = "Outer X"
is nonlocal because it's inside theouter()
function, and it can be accessed byinner()
ifnonlocal
is used.inner()
modifies nonlocal X becausenonlocal
allows it to change the variable defined inouter()
.
-
Local Variables:
- Declared inside a function.
- Accessible only within that function.
-
Nonlocal Variables:
- Declared in an enclosing function.
- Accessible inside nested functions, but not in the global scope.
-
Global Variables:
- Declared outside of all functions.
- Accessible anywhere in the program file.
-
Lexical Scoping ensures that variable access is determined by where the variable is declared, not where it is used.
Python follows the LEGB rule to resolve variable names:
- Local (L): Variables defined inside the current function.
- Enclosing (E): Variables in the outer function (if the current function is nested).
- Global (G): Variables defined at the module level.
- Built-in (B): Predefined Python functions and constants.
# Global Scope
name = "Muhammad Hashim" # Global variable
def outer():
name = "Hashim" # Enclosing (Nonlocal) variable
def inner():
name = "Local Hashim" # Local variable
print(f"Inside inner: {name}")
inner()
print(f"Inside outer: {name}")
outer()
print(f"Global scope: {name}")
Inside inner: Local Hashim
Inside outer: Hashim
Global scope: Muhammad Hashim
inner()
function prints"Local Hashim"
becausename
is a local variable insideinner
.outer()
function prints"Hashim"
becausename
refers to the enclosing nonlocal variable inouter
.- Global scope prints
"Muhammad Hashim"
, the global variable.
-
Assignments Create Local Variables:
- When you assign a variable inside a function, Python treats it as local unless explicitly declared otherwise.
- Example:
def test(): x = 10 # Local variable print(x) test() print(x) # Error: x is not defined (outside local scope)
-
Using
global
:- The
global
keyword tells Python to use a global variable instead of creating a new local variable. - Example:
count = 0 # Global variable def increment(): global count count += 1 increment() print(count) # Output: 1
- Explanation: The
global
keyword allowsincrement
to modify thecount
variable globally.
- The
-
Using
nonlocal
:- The
nonlocal
keyword is used to refer to enclosing variables from the outer function. - Example:
def outer(): count = 0 def inner(): nonlocal count count += 1 print(f"Inner count: {count}") inner() print(f"Outer count: {count}") outer() # Output: # Inner count: 1 # Outer count: 1
- Explanation: The
nonlocal
keyword allowsinner
to modifycount
inouter
, so the change is visible in both scopes.
- The
# Global Scope
X = 99 # Global variable
def func(Y): # Y is a local variable
# Local Scope
Z = X + Y # X is global, Y and Z are local
return Z
result = func(1) # func is called with Y=1
print(result) # Output: 100
- Global Scope:
X
is global because it's defined at the module level.func
is also global, available throughout the module.
- Local Scope:
Y
andZ
are local tofunc()
, created and used only whenfunc()
runs.
- Variable References:
- When
Z = X + Y
is executed, Python uses the LEGB rule:X
is found in the global scope.Y
is found in the local scope.Z
is assigned locally and can only be accessed withinfunc()
.
- When
In Python, there are several built-in functions, constants, and modules that you can use without importing anything. These are available because they reside in the built-in scope. Let's discuss how Python handles the built-in scope, what it means to redefine these built-ins, and some best practices to avoid common pitfalls.
The built-in scope in Python consists of names that are predefined and always available. This includes:
- Functions like
print()
,len()
,open()
- Constants like
True
,False
,None
- Exceptions like
ValueError
,TypeError
Python will check the built-in scope last when it tries to resolve a variable name, following the LEGB rule:
- Local: Inside the current function.
- Enclosing: In the local scope of any enclosing function.
- Global: At the module level.
- Built-in: Predefined names.
To get a list of all built-in names in Python, you can use the dir()
function on the builtins
module:
import builtins
print(dir(builtins))
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', ... ]
This will print a list of all available functions, exceptions, and constants in the built-in scope.
Python allows you to redefine built-in names, but doing so can lead to unexpected behavior. Here’s why:
- If you create a variable named
open
, you hide the built-inopen()
function. - Once overridden, you can't use the original
open()
function unless you specifically reference it from thebuiltins
module.
# Redefine the built-in open function
open = "This is not a function anymore!" # Overrides the built-in open
print(open) # Output: This is not a function anymore!
# Now, try to use open() to open a file:
try:
open("example.txt", "r")
except TypeError as e:
print(f"Error: {e}")
This is not a function anymore!
Error: 'str' object is not callable
open
is redefined as a string. This hides the built-inopen()
function within the local or global scope.- Trying to use
open()
after redefinition causes a TypeError becauseopen
is no longer a function; it's a string.
If you accidentally override a built-in name, you can still access the original function using the builtins
module:
import builtins
# Safely using built-ins after overriding
open = "Overridden Open" # Overrides the built-in open
print(open) # Prints the string
# Access the original built-in open
with builtins.open("example.txt", "w") as file:
file.write("This is safe!")
- Explanation: The
builtins.open
lets you safely access the originalopen
function even after it’s been redefined locally.
- Choose unique variable names that do not clash with built-ins.
- Avoid using built-in names such as
list
,str
,sum
, etc., for your variables. - Restore built-in functionality if overridden by using
del
:list = [1, 2, 3] # Overrides built-in list function print(list) # Output: [1, 2, 3] del list # Remove override print(list("123")) # Built-in list function works again
# Safely using built-ins after overriding
import builtins
def process_data():
len = "Overridden Length" # Override the built-in len function
print(len) # Prints the string
print(builtins.len([1, 2, 3, 4])) # Safely use the original len function
process_data()
Overridden Length
4
When programming in Python, it’s crucial to manage how your functions handle variables to avoid common pitfalls. While global variables can be useful, overreliance on them may lead to bugs and difficult-to-maintain code. Let’s explore the concept of global variables, why we should minimize their use, and the preferred way of passing data using function arguments and return values. We'll also see practical examples featuring Muhammad Hashim.
- Global variables are defined at the top level of a script or module, making them accessible throughout the entire file.
- These variables can be read or modified by any function within the same module, leading to potential issues when multiple functions modify them.
age = 25 # Global variable
def print_age():
print(f"Muhammad Hashim is {age} years old.") # Accessing global variable inside a function
print_age() # Output: Muhammad Hashim is 25 years old.
- Explanation: The variable
age
is defined globally, making it accessible toprint_age()
. However, ifage
were to be modified elsewhere, it could cause unintended side effects.
To modify a global variable inside a function, you need to use the global
statement. Without it, Python treats the variable as local.
experience = 3 # Global variable representing years of experience
def update_experience():
global experience # Declare as global
experience = 5 # Modify the global variable
print("Before:", experience) # Output: 3
update_experience()
print("After:", experience) # Output: 5
- Explanation: The
global
statement allowsupdate_experience()
to modify the global variableexperience
. However, overusing global variables can lead to harder-to-debug code.
Global variables can cause several issues:
- Hard to Debug: Changes in one part of the program can affect other parts unexpectedly.
- Difficult to Reuse Code: Functions relying on global variables are harder to reuse because they depend on data defined outside their scope.
- Unintended Overwrites: Accidentally modifying a global variable can lead to unexpected behavior.
# Best Practice: Use function arguments and return values instead of global variables. This leads to cleaner and more maintainable code.
Instead of relying on global variables, a better approach is to use function arguments to pass data into functions and return values to get data out of functions. This ensures that each function is self-contained and predictable.
# Recommended
def calculate_age(birth_year):
current_year = 2024
return current_year - birth_year
muhammad_age = calculate_age(1999)
print(f"Muhammad Hashim is {muhammad_age} years old.") # Output: Muhammad Hashim is 25 years old.
- Explanation: Here,
birth_year
is passed as an argument tocalculate_age
, making the function self-contained and flexible. The function doesn’t rely on any external data, and its behavior is predictable.
Suppose you want to calculate Muhammad Hashim's yearly savings based on income and expenses. Instead of using global variables, let’s see how you can write clean and modular code using function arguments.
# Not recommended
monthly_income = 5000
monthly_expenses = 3000
def calculate_monthly_savings():
return monthly_income - monthly_expenses
def predict_yearly_savings():
return calculate_monthly_savings() * 12
print(f"Predicted Yearly Savings: {predict_yearly_savings()}") # Output: 24000
- Explanation: The functions depend on global variables, making them harder to reuse or test with different inputs.
# Recommended
def calculate_monthly_savings(income, expenses):
return income - expenses
def predict_yearly_savings(income, expenses):
monthly_savings = calculate_monthly_savings(income, expenses)
return monthly_savings * 12
income = 5000
expenses = 3000
yearly_savings = predict_yearly_savings(income, expenses)
print(f"Predicted Yearly Savings: {yearly_savings}") # Output: 24000
- Explanation: By passing
income
andexpenses
as arguments, the functions are now independent of any external data. They can be reused or tested with different inputs without any changes to the global state.
Global variables can be powerful but should be used carefully to maintain clean, efficient, and error-free code. Let’s explore how they work, their best practices, and how to use them effectively in Python with examples featuring Muhammad Hashim.
- Global variables are defined at the top level of a module or script, making them accessible throughout the entire file. They allow data to be shared across different functions.
- Python’s LEGB (Local, Enclosing, Global, Built-in) rule determines how variables are searched for. Global variables fall under the Global (G) scope.
age = 25 # Global variable
def print_age():
print(f"Muhammad Hashim is {age} years old.") # Accessing global variable inside a function
You can access global variables from other modules, but this should be done cautiously.
-
File 1:
muhammad.py
city = "Lahore" # Global variable in muhammad.py
-
File 2:
details.py
import muhammad print(f"Muhammad Hashim lives in {muhammad.city}.") # Access global variable from muhammad.py muhammad.city = "Islamabad" # Modify global variable from muhammad.py
-
Explanation: When
details.py
importsmuhammad
, it can access and modifycity
. This can lead to cross-file dependencies, which may make the code difficult to manage.
Instead of directly modifying global variables, it's better to manage changes through functions. This makes the code clearer and easier to maintain.
-
File 1:
muhammad.py
skill_level = "Intermediate" def set_skill_level(level): global skill_level skill_level = level # Modify global variable explicitly def get_skill_level(): return skill_level
-
File 2:
details.py
import muhammad print(f"Muhammad Hashim's skill level: {muhammad.get_skill_level()}.") # Output: Intermediate muhammad.set_skill_level("Expert") # Modify using accessor function print(f"Updated skill level: {muhammad.get_skill_level()}.") # Output: Expert
-
Explanation: Using
set_skill_level()
andget_skill_level()
makes it explicit that any changes happen through controlled functions.
Global variables can be used as shared memory between threads. When multiple threads access and modify the same global variable, it may lead to race conditions. Always use thread synchronization (e.g., threading.Lock
) when working with threads and global variables.
Another way to manage global variables is to access them as module attributes. Here’s an example that demonstrates this:
-
File:
hashim.py
import sys projects_completed = 15 # Global variable def increment_projects(): sys.modules[__name__].projects_completed += 1 # Access and modify global variable
-
Explanation: Using
sys.modules
lets you access the module itself, which allows for modifying global variables explicitly.
# File: config.py
settings = {
"name": "Muhammad Hashim",
"profession": "Software Engineer",
"projects": 10
}
def update_settings(key, value):
global settings
if key in settings:
settings[key] = value
else:
raise KeyError("Key does not exist in settings")
# File: app.py
from config import settings, update_settings
print("Before Update:", settings)
update_settings("projects", 12)
print("After Update:", settings)
Before Update: {'name': 'Muhammad Hashim', 'profession': 'Software Engineer', 'projects': 10}
After Update: {'name': 'Muhammad Hashim', 'profession': 'Software Engineer', 'projects': 12}
- Explanation: This approach of managing settings through functions allows clear, controlled updates and avoids accidental modifications.
A nested function is a function defined inside another function. This creates an enclosing scope—the outer function's local variables can be accessed by the inner function.
def greet():
message = "Hello, Muhammad Hashim!" # Enclosing scope
def print_message():
print(message) # Accessing 'message' from enclosing scope
print_message()
greet() # Output: Hello, Muhammad Hashim!
- Explanation: The inner function
print_message
can accessmessage
, even thoughmessage
is not defined within it. This is due to the Enclosing scope created bygreet()
.
The global
keyword allows you to modify a global variable from within a function. Without declaring it as global
, Python treats it as a local variable.
counter = 0
def increment():
counter += 1 # Error: Python treats 'counter' as local, which is not yet assigned
increment() # Raises UnboundLocalError
counter = 0 # Global variable
def increment():
global counter # Declaring 'counter' as global
counter += 1
increment()
print(counter) # Output: 1
- Explanation: Using
global
tells Python that thecounter
insideincrement()
refers to the globalcounter
.
The nonlocal
keyword is used to modify variables in the nearest enclosing scope. This allows inner functions to change variables defined in their enclosing function.
def outer():
age = 25 # Enclosing scope
def inner():
nonlocal age # Declaring 'age' as nonlocal
age = 30 # Modify the enclosing 'age'
inner()
print(f"Muhammad Hashim's updated age: {age}")
outer() # Output: Muhammad Hashim's updated age: 30
- Explanation: By declaring
age
asnonlocal
, the inner function can modifyage
inouter()
.
Closures are nested functions that "remember" values from their enclosing scopes even after the outer function has completed execution. This can be a useful way to create functions with customized behavior.
def multiplier(factor):
def multiply(number):
return number * factor # 'factor' is remembered from enclosing scope
return multiply
double = multiplier(2)
print(double(5)) # Output: 10
triple = multiplier(3)
print(triple(5)) # Output: 15
- Explanation:
double
andtriple
are closures created bymultiplier(2)
andmultiplier(3)
, respectively.- Each closure "remembers" its own
factor
, allowing you to reusemultiplier
flexibly.
If you create functions within a loop, be cautious—loop variables may not behave as you expect. To avoid unexpected behavior, use default arguments.
def create_actions():
actions = []
for i in range(3):
actions.append(lambda: print(i)) # Each lambda will reference the same 'i'
return actions
actions = create_actions()
actions[0]() # Output: 2
actions[1]() # Output: 2
actions[2]() # Output: 2
- Problem: Each lambda references
i
as it was when the loop ended, which is2
.
def create_actions():
actions = []
for i in range(3):
actions.append(lambda i=i: print(i)) # Capture 'i' at each iteration
return actions
actions = create_actions()
actions[0]() # Output: 0
actions[1]() # Output: 1
actions[2]() # Output: 2
- Explanation: By setting
i=i
in the lambda, each function captures its own value ofi
.
In Python, the nonlocal
keyword is an advanced tool that allows a nested function to modify variables in its enclosing function's scope. This feature, available only in Python 3.x, provides greater flexibility when dealing with nested functions by allowing them to read and write to variables in the surrounding function.
Unlike global
, which affects the module-level scope, nonlocal
restricts its operation to enclosing function scopes, enabling more controlled state management without using global variables.
The nonlocal
statement enables a function defined within another function (a nested function) to access and modify variables from the outer function's scope. Without nonlocal
, these variables are read-only by default; attempting to modify them would cause an error.
-
nonlocal
Statement Syntaxdef outer(): x = 10 # Enclosing scope def inner(): nonlocal x # Declare 'x' as nonlocal x += 1 inner() print(x) # Output: 11 outer()
-
Explanation:
- The inner function can both read and modify
x
fromouter()
because ofnonlocal
. Withoutnonlocal
, Python would treatx
as local toinner()
, leading to an error.
- The inner function can both read and modify
The nonlocal
keyword is especially useful for state retention within nested functions, enabling functions to carry out tasks like counters, trackers, and other tasks requiring persistent state between function calls.
def create_counter(start=25):
age = start # Enclosing variable
def increment():
nonlocal age # Declaring 'age' as nonlocal
age += 1 # Modify the enclosing 'age' variable
print(f"Muhammad Hashim's age is now: {age}")
return increment
counter = create_counter() # Starting age at 25
counter() # Output: Muhammad Hashim's age is now: 26
counter() # Output: Muhammad Hashim's age is now: 27
- Explanation:
create_counter()
initializesage
to 25.- The nested
increment
function can modifyage
because it is markednonlocal
. - Each call to
counter()
increments theage
.
Feature | nonlocal |
global |
---|---|---|
Scope | Enclosing function | Module-level (global) |
Can Modify? | Yes (only in enclosing functions) | Yes (affects global variables) |
Must Exist? | Yes (must be pre-defined in the enclosing scope) | No (can be created dynamically) |
Usage Context | Better for retaining state within nested functions | Suitable for values shared across modules |
def counter():
count = 0 # Enclosing variable
def increment():
count += 1 # Error: 'count' is treated as local here
print(count)
increment() # Raises UnboundLocalError
counter()
- Error Explanation: Since
count
is modified, Python treats it as local insideincrement()
. This results in an UnboundLocalError because it is not defined as a local variable.
Factory functions can create closures—nested functions that "remember" variables from the enclosing scope. Here’s how nonlocal
can be used to manage persistent state.
def progress_tracker(initial_steps=0):
steps = initial_steps # Enclosing variable
def track():
nonlocal steps # Allow modifying 'steps' from the enclosing scope
steps += 10
print(f"Muhammad Hashim has completed {steps} steps!")
return track
tracker = progress_tracker() # Initialize with 0 steps
tracker() # Output: Muhammad Hashim has completed 10 steps!
tracker() # Output: Muhammad Hashim has completed 20 steps!
another_tracker = progress_tracker(100) # Start from 100 steps
another_tracker() # Output: Muhammad Hashim has completed 110 steps!
- Explanation:
progress_tracker
initializessteps
.- Each call to
tracker()
modifiessteps
because it is declared asnonlocal
, ensuring the state is persistent. - Multiple counters (
tracker
,another_tracker
) maintain independent states.
- Declaration: When you declare a variable as
nonlocal
, Python skips the local scope and starts searching for the variable in the enclosing scope. - Modification: This allows modification of the variable instead of treating it as read-only.
- Error Handling: The variable must already exist in the enclosing function; otherwise, Python will raise a SyntaxError.
def outer_function():
def inner_function():
nonlocal value # Error: 'value' does not exist in the enclosing scope
value = 10
inner_function()
outer_function() # Raises SyntaxError
- Error Explanation: Since
value
is not defined inouter_function
,nonlocal
cannot reference it.nonlocal
does not create new variables.
Before nonlocal
was introduced, programmers often relied on classes, globals, and function attributes for state management.
def create_stateful():
def inner():
inner.counter += 1
print(f"Counter: {inner.counter}")
inner.counter = 0 # Initialize attribute
return inner
counter_function = create_stateful()
counter_function() # Output: Counter: 1
counter_function() # Output: Counter: 2
- Explanation: The
inner
function is modified directly usinginner.counter
, allowing state management withoutnonlocal
.