Welcome to an in-depth exploration of variables and expressions in Python! This journey will take you through the versatile world of Python's variable system, complete with duck-typing, None values, and practical applications that will serve you throughout your programming career.
By the end of this tutorial, you'll confidently be able to:
None as a powerful tool in your programsThink of Python variables as labeled storage containers in a vast warehouse of computer memory. Unlike some warehouses that only allow specific items in specific containers, Python's warehouse is remarkably flexible.
Each container (variable) has a name tag (the variable name) and can store virtually any type of content (the value). What makes Python special is how easily you can change both what's in the container and even what type of content it holds.
Python embraces a philosophy called duck-typing, derived from the saying: "If it walks like a duck and quacks like a duck, then it probably is a duck." In programming terms, this means Python cares more about what an object can do rather than what it is.
Duck-typing allows Python to be remarkably flexible. Consider this example:
def print_all_items(collection):
for item in collection:
print(item)
# Works with a list
print_all_items([1, 2, 3])
# Also works with a tuple
print_all_items((4, 5, 6))
# Even works with a string
print_all_items("Python")
# And works with a dictionary (iterates through keys)
print_all_items({"a": 1, "b": 2})
The print_all_items function doesn't care what type collection is. It only cares that it can iterate over it. This is duck-typing in action: "If it can be iterated over like a collection, we'll treat it as a collection."
Python programmers often follow a principle called EAFP: "Easier to Ask for Forgiveness than Permission." This means rather than checking if an operation is possible beforehand, we attempt the operation and handle any exceptions that arise.
# Non-Pythonic way (LBYL - Look Before You Leap)
if isinstance(obj, list) and len(obj) > 0:
first_item = obj[0]
else:
first_item = None
# Pythonic way (EAFP)
try:
first_item = obj[0]
except (IndexError, TypeError):
first_item = None
The EAFP approach is generally more concise and handles edge cases better, especially in a language like Python with duck-typing.
In Python's standard library, many functions that work with files don't actually require a file object—they just need something that behaves like one:
import io
# A string buffer that acts like a file
fake_file = io.StringIO("Hello, duck-typing world!")
# Reading from our fake file
content = fake_file.read()
print(content) # Outputs: Hello, duck-typing world!
# This function works with real files AND file-like objects
def read_first_line(file_obj):
original_position = file_obj.tell() # Save position
file_obj.seek(0) # Go to start
first_line = file_obj.readline()
file_obj.seek(original_position) # Restore position
return first_line
# Works with our fake file
print(read_first_line(fake_file)) # Outputs: Hello, duck-typing world!
# Would also work with a real file
with open("real_file.txt", "w+") as real_file:
real_file.write("This is a real file.\nWith multiple lines.")
print(read_first_line(real_file)) # Outputs: This is a real file.
This flexibility is powerful: it means you can create custom objects that work with existing functions, as long as they implement the necessary methods.
Unlike languages like JavaScript or Java, Python doesn't use keywords like var, let, or const to declare variables. Instead, a variable is created the moment you assign a value to it.
# Creating variables with simple assignment
username = "pythonista"
score = 95
is_active = True
# Python automatically determines the appropriate type
print(type(username)) # <class 'str'>
print(type(score)) # <class 'int'>
print(type(is_active)) # <class 'bool'>
Think of variable assignment as labeling a box with contents. When you write score = 95, you're telling Python: "Take this value 95 and put a label called 'score' on it."
Python offers several elegant ways to assign multiple variables at once:
# Assigning same value to multiple variables
x = y = z = 0
print(x, y, z) # 0 0 0
# Unpacking values
coordinates = (10, 20)
x, y = coordinates
print(x, y) # 10 20
# Swapping values (without temporary variable)
a, b = 5, 10
a, b = b, a
print(a, b) # 10 5
# Extended unpacking (Python 3+)
first, *middle, last = [1, 2, 3, 4, 5]
print(first) # 1
print(middle) # [2, 3, 4]
print(last) # 5
The unpacking technique is particularly useful when working with functions that return multiple values:
def get_user_info():
return "Alice", 28, "Developer"
# Unpack the returned tuple into separate variables
name, age, occupation = get_user_info()
print(f"{name} is a {age}-year-old {occupation}")
# Outputs: Alice is a 28-year-old Developer
One of Python's most distinctive features is how it allows variables to change types. While this flexibility is powerful, it also requires careful attention to your code's logic.
counter = 0 # counter is an integer
print(counter) # 0
counter = "Zero" # Now counter is a string
print(counter) # Zero
counter = [0, 1] # Now counter is a list
print(counter) # [0, 1]
Imagine a variable as a name tag that can be moved from one object to another. When you reassign a variable, you're not changing the original object—you're simply moving the name tag to a different object.
While Python allows type changes, following certain conventions makes your code more maintainable:
# Using type hints for clarity (Python 3.5+)
def calculate_total(prices: list) -> float:
"""Calculate the sum of all prices."""
return sum(prices)
# Even with type hints, Python still allows flexibility
result = calculate_total([10.99, 24.50, 5.00]) # Works as expected
print(result) # 40.49
# This will run (but might cause logical errors later)
alternative_result = calculate_total("123") # Sums ASCII values!
print(alternative_result) # 150 (ASCII: '1' = 49, '2' = 50, '3' = 51)
Python provides a variety of operators and shortcuts to manipulate variables efficiently.
These operators combine an operation with assignment, making your code more concise:
# Starting value
count = 10
# Arithmetic augmented assignments
count += 5 # Addition (count = count + 5)
print(count) # 15
count -= 3 # Subtraction (count = count - 3)
print(count) # 12
count *= 2 # Multiplication (count = count * 2)
print(count) # 24
count /= 4 # Division (count = count / 4)
print(count) # 6.0 (note: converts to float)
count //= 2 # Floor division (count = count // 2)
print(count) # 3.0 (still a float)
count **= 2 # Exponentiation (count = count ** 2)
print(count) # 9.0
count %= 5 # Modulo (count = count % 5)
print(count) # 4.0
These operators aren't limited to numbers. Many work with other types as well:
# String augmented assignment
greeting = "Hello"
greeting += ", World!" # String concatenation
print(greeting) # Hello, World!
# List augmented assignment
numbers = [1, 2, 3]
numbers += [4, 5] # List concatenation
print(numbers) # [1, 2, 3, 4, 5]
numbers *= 2 # List replication
print(numbers) # [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
Often, you'll need to combine multiple operations. Python's expressions can be as simple or complex as needed:
# Calculate area of a circle
radius = 5
pi = 3.14159
area = pi * (radius ** 2)
print(f"Area of circle: {area}") # Area of circle: 78.53975
# String formatting with variables
first_name = "Ada"
last_name = "Lovelace"
full_name = f"{first_name} {last_name}"
print(full_name) # Ada Lovelace
# List comprehension (creating a list from an expression)
squares = [n ** 2 for n in range(1, 6)]
print(squares) # [1, 4, 9, 16, 25]
None Value: Python's Representation of Nothing
In Python, None is a special constant that represents the absence of a value or a null value. It serves a similar purpose to null in many other programming languages. However, JavaScript has both null and undefined, while Python just has None.
None
None is particularly useful in several scenarios:
# Function that might not find what it's looking for
def find_user(username, user_database):
for user in user_database:
if user.get('username') == username:
return user
return None # Explicitly return None when user not found
# Check for None with 'is' operator (not ==)
result = find_user("alice", users_list)
if result is None:
print("User not found")
else:
print(f"Found user: {result}")
is None vs == None
When checking for None, always use the is operator rather than ==. The is operator checks for identity (whether two variables point to the same object), while == checks for equality (whether two variables have the same value).
# The correct way to check for None
if variable is None:
print("Variable is None")
# The correct way to check for not None
if variable is not None:
print("Variable has a value")
Since None is a singleton (only one instance exists in the entire Python program), identity comparison with is is both more correct and slightly faster.
If a function doesn't explicitly return a value, it automatically returns None:
def greet(name):
print(f"Hello, {name}!")
# No return statement
result = greet("World")
print(result) # None
This is also true for a bare return statement with no value:
def early_exit_function():
if error_condition:
return # Returns None
# Rest of function...
Unlike JavaScript, which returns NaN ("Not a Number") for many invalid operations, Python prefers to throw explicit errors rather than silently continue with potentially problematic values.
Understanding these errors helps you debug your code more efficiently:
# NameError: Trying to use a variable that doesn't exist
print(undefined_variable) # NameError: name 'undefined_variable' is not defined
# TypeError: Performing an operation with incompatible types
number = "42"
result = number / 2 # TypeError: unsupported operand type(s) for /: 'str' and 'int'
# ValueError: Valid type but invalid value
int("not a number") # ValueError: invalid literal for int() with base 10
# IndexError: Accessing an index that doesn't exist
my_list = [1, 2, 3]
print(my_list[10]) # IndexError: list index out of range
# KeyError: Accessing a dictionary key that doesn't exist
my_dict = {"a": 1, "b": 2}
print(my_dict["c"]) # KeyError: 'c'
Python's exception handling allows you to gracefully manage errors that might occur when working with variables:
# Safer conversion from string to number
def safe_convert_to_int(value):
try:
return int(value)
except ValueError:
print(f"Warning: '{value}' is not a valid integer")
return None
# Testing our function
print(safe_convert_to_int("42")) # 42
print(safe_convert_to_int("3.14")) # Warning: '3.14' is not a valid integer
# None
print(safe_convert_to_int("Hello")) # Warning: 'Hello' is not a valid integer
# None
When building robust applications, it's important to anticipate potential variable issues:
# Safe dictionary access
def get_user_preference(user_profile, preference_name, default_value=None):
"""Safely get a user preference or return default if not found."""
if user_profile is None:
return default_value
try:
return user_profile['preferences'][preference_name]
except (KeyError, TypeError):
return default_value
# Example usage
user = {
'name': 'Alex',
'preferences': {
'theme': 'dark',
'notifications': True
}
}
empty_user = None
print(get_user_preference(user, 'theme')) # dark
print(get_user_preference(user, 'language', 'en')) # en (default)
print(get_user_preference(empty_user, 'theme', 'light')) # light (default)
Let's explore how these concepts apply in practical, real-world programming scenarios:
When processing data from external sources, you often deal with missing or inconsistent values:
def clean_customer_record(record):
"""Process a customer record, filling in missing values appropriately."""
# Initialize a clean record with defaults
clean_record = {
'id': None,
'name': 'Unknown',
'email': None,
'age': None,
'is_active': False,
'last_purchase': None
}
# If no record provided, return defaults
if record is None:
return clean_record
# Transfer and validate each field
if 'id' in record and record['id']:
clean_record['id'] = str(record['id']) # Ensure ID is string
if 'name' in record and record['name']:
clean_record['name'] = record['name'].strip()
if 'email' in record:
email = record['email']
if email and '@' in email:
clean_record['email'] = email.lower().strip()
# Handle age with validation
if 'age' in record:
try:
age = int(record['age'])
if 0 <= age <= 120: # Basic age validation
clean_record['age'] = age
except (ValueError, TypeError):
pass # Keep as None if invalid
# Boolean conversion
if 'is_active' in record:
clean_record['is_active'] = bool(record['is_active'])
# Date handling
if 'last_purchase' in record and record['last_purchase']:
# Just store as is for this example
clean_record['last_purchase'] = record['last_purchase']
return clean_record
# Test with various record qualities
perfect_record = {
'id': 1001,
'name': 'John Smith',
'email': 'john@example.com',
'age': 34,
'is_active': True,
'last_purchase': '2025-01-15'
}
partial_record = {
'id': 1002,
'name': ' Jane Doe ',
'age': 'unknown'
}
print(clean_customer_record(perfect_record))
print(clean_customer_record(partial_record))
print(clean_customer_record(None))
Managing application settings with a mix of defaults, environment variables, and user preferences:
import os
def load_configuration():
"""Load application configuration from multiple sources."""
# Default configuration
config = {
'debug': False,
'port': 8080,
'db_host': 'localhost',
'db_port': 5432,
'log_level': 'info',
'max_connections': 100,
'timeout': 30
}
# Override with environment variables if they exist
if 'APP_DEBUG' in os.environ:
debug_val = os.environ['APP_DEBUG'].lower()
config['debug'] = debug_val in ('true', 'yes', '1', 'y')
if 'APP_PORT' in os.environ:
try:
config['port'] = int(os.environ['APP_PORT'])
except ValueError:
print(f"Warning: Invalid APP_PORT value: {os.environ['APP_PORT']}")
# Database configuration
for key in ('db_host', 'db_port'):
env_key = f"APP_{key.upper()}"
if env_key in os.environ:
if key == 'db_port':
try:
config[key] = int(os.environ[env_key])
except ValueError:
print(f"Warning: Invalid {env_key} value")
else:
config[key] = os.environ[env_key]
# Try to load from configuration file
config_file = os.environ.get('CONFIG_FILE', 'config.json')
try:
with open(config_file, 'r') as f:
import json
file_config = json.load(f)
# Update our config with file values
config.update(file_config)
except (FileNotFoundError, json.JSONDecodeError):
# It's okay if the file doesn't exist or isn't valid JSON
pass
return config
# Usage
app_config = load_configuration()
print(f"Starting server on port {app_config['port']} with debug={app_config['debug']}")
Using flexible variable handling for data preprocessing:
def preprocess_features(features, normalize=True, fill_missing=True):
"""Preprocess feature data for machine learning."""
import statistics
if features is None or len(features) == 0:
return []
processed_features = []
# Calculate statistics for each feature column
column_stats = {}
for i in range(len(features[0])):
# Extract column values, filtering out None
column_values = [row[i] for row in features if i < len(row) and row[i] is not None]
if column_values:
column_stats[i] = {
'mean': statistics.mean(column_values),
'median': statistics.median(column_values),
'min': min(column_values),
'max': max(column_values),
'range': max(column_values) - min(column_values) if len(column_values) > 1 else 1
}
else:
# Default stats if no valid values
column_stats[i] = {
'mean': 0,
'median': 0,
'min': 0,
'max': 0,
'range': 1
}
# Process each row
for row in features:
processed_row = []
for i in range(len(row)):
value = row[i]
# Handle missing values
if value is None and fill_missing:
value = column_stats[i]['median'] # Use median for missing values
# Normalize if requested
if normalize and value is not None:
stats = column_stats[i]
# Min-max normalization
if stats['range'] != 0:
value = (value - stats['min']) / stats['range']
else:
value = 0.5 # Default when all values are the same
processed_row.append(value)
processed_features.append(processed_row)
return processed_features
# Example usage
raw_features = [
[23.1, 5.2, 10.0],
[19.7, None, 8.5],
[25.3, 4.8, 11.2],
[22.0, 5.0, None]
]
processed = preprocess_features(raw_features)
print("Processed features:")
for row in processed:
print(row)
Let's reinforce your understanding with some hands-on practice activities:
Create a function that takes any Python object and explores its properties, returning a report about the variable.
def explore_variable(variable):
"""
Explore and report on the properties of a Python variable.
Returns a dictionary with information about the variable.
"""
report = {
'type': type(variable).__name__,
'memory_address': id(variable),
'is_none': variable is None,
}
# Add type-specific information
if isinstance(variable, (int, float, complex)):
report['category'] = 'numeric'
if isinstance(variable, int):
report['is_even'] = variable % 2 == 0
report['is_positive'] = variable > 0
elif isinstance(variable, str):
report['category'] = 'string'
report['length'] = len(variable)
report['is_empty'] = len(variable) == 0
report['is_alphanumeric'] = variable.isalnum() if variable else False
elif isinstance(variable, (list, tuple, set)):
report['category'] = 'collection'
report['length'] = len(variable)
report['is_empty'] = len(variable) == 0
if variable: # If not empty
# Check if all elements are of the same type
first_type = type(next(iter(variable)))
report['uniform_type'] = all(isinstance(item, first_type) for item in variable)
report['element_types'] = list(set(type(item).__name__ for item in variable))
elif isinstance(variable, dict):
report['category'] = 'mapping'
report['length'] = len(variable)
report['keys'] = list(variable.keys())
elif callable(variable):
report['category'] = 'callable'
report['is_function'] = callable(variable)
import inspect
report['takes_arguments'] = len(inspect.signature(variable).parameters) > 0 if inspect.isfunction(variable) else 'unknown'
return report
# Test with different types
test_cases = [
42,
"Hello, World!",
[1, 2, 3, "four"],
{"a": 1, "b": 2},
None,
lambda x: x*2
]
for case in test_cases:
report = explore_variable(case)
print(f"Report for {case}:")
for key, value in report.items():
print(f" {key}: {value}")
print()
Create a class that demonstrates Python's variable flexibility by shape-shifting between different types based on operations performed on it.
class ShapeShifter:
"""
A class that changes its behavior and type based on operations.
Demonstrates Python's dynamic typing.
"""
def __init__(self, value=None):
self.value = value
def __repr__(self):
return f"ShapeShifter({self.value})"
def __str__(self):
return str(self.value) if self.value is not None else "None"
# Numeric operations
def __add__(self, other):
if isinstance(other, ShapeShifter):
other = other.value
if self.value is None:
self.value = other
elif isinstance(self.value, (int, float)) and isinstance(other, (int, float)):
self.value += other
elif isinstance(self.value, str) and isinstance(other, str):
self.value += other
elif isinstance(self.value, list):
if isinstance(other, list):
self.value.extend(other)
else:
self.value.append(other)
else:
# Convert to string and concatenate
self.value = str(self.value) + str(other)
return self
# Multiplication
def __mul__(self, other):
if isinstance(other, ShapeShifter):
other = other.value
if isinstance(self.value, (int, float)) and isinstance(other, (int, float)):
self.value *= other
elif isinstance(self.value, str) and isinstance(other, int):
self.value *= other
elif isinstance(self.value, list) and isinstance(other, int):
self.value = self.value * other
else:
# Default behavior: convert to string
self.value = str(self.value) * (other if isinstance(other, int) else 1)
return self
# Boolean behavior
def __bool__(self):
return bool(self.value)
# List-like behavior
def __getitem__(self, index):
if self.value is None:
raise IndexError("Cannot index None value")
if not hasattr(self.value, '__getitem__'):
# Convert to string if not indexable
self.value = str(self.value)
return self.value[index]
# Dictionary-like behavior
def keys(self):
if not isinstance(self.value, dict):
# Convert to dict if not already
if isinstance(self.value, list):
self.value = {i: item for i, item in enumerate(self.value)}
else:
self.value = {"value": self.value}
return self.value.keys()
def clear(self):
"""Reset to None"""
self.value = None
return self
# Test the ShapeShifter
shifter = ShapeShifter(5)
print(f"Initial: {shifter}") # 5
# Number operations
shifter = shifter + 10
print(f"After addition: {shifter}") # 15
shifter = shifter * 2
print(f"After multiplication: {shifter}") # 30
# Convert to string via concatenation with string
shifter = shifter + " points"
print(f"After string concat: {shifter}") # "30 points"
# String operations
shifter = shifter * 2
print(f"After string replication: {shifter}") # "30 points30 points"
# Access like a string
print(f"First character: {shifter[0]}") # "3"
# Convert to list
shifter.clear()
shifter = shifter + [1, 2, 3]
print(f"As list: {shifter}") # [1, 2, 3]
# List operations
shifter = shifter + [4, 5]
print(f"After list extend: {shifter}") # [1, 2, 3, 4, 5]
shifter = shifter * 2
print(f"After list replication: {shifter}") # [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
# Convert to dictionary
print(f"Keys: {list(shifter.keys())}") # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In this activity, you'll create a function that analyzes a Python script to identify variable usage patterns, potential errors, and optimization opportunities.
def profile_script(script_text):
"""
Analyze a Python script and profile its variable usage.
Returns a report with variable analysis.
"""
import ast
import re
report = {
'variables': {},
'potential_issues': [],
'statistics': {
'total_variables': 0,
'reassigned_variables': 0,
'type_changed_variables': 0,
'unused_variables': 0,
'none_assignments': 0
}
}
# Simple pattern matching for variable assignments
assignment_pattern = re.compile(r'(\w+)\s*=\s*(.+)')
assignments = [match for match in assignment_pattern.findall(script_text)]
# Track all variable assignments
for var_name, value in assignments:
if var_name not in report['variables']:
report['variables'][var_name] = {
'assignments': [],
'potential_type_changes': False,
'assigned_none': False
}
report['variables'][var_name]['assignments'].append(value.strip())
# Check for None assignments
if 'None' in value:
report['variables'][var_name]['assigned_none'] = True
report['statistics']['none_assignments'] += 1
# Analyze for type changes
for var_name, details in report['variables'].items():
assignments = details['assignments']
if len(assignments) > 1:
report['statistics']['reassigned_variables'] += 1
# Simple heuristic for type changes
has_string = any('"' in a or "'" in a for a in assignments)
has_number = any(a.isdigit() or a.replace('.', '', 1).isdigit() for a in assignments if a.strip() not in ['True', 'False', 'None'])
has_list = any('[' in a and ']' in a for a in assignments)
has_dict = any('{' in a and '}' in a for a in assignments)
has_bool = any(a.strip() in ['True', 'False'] for a in assignments)
has_none = any(a.strip() == 'None' for a in assignments)
# Count different types
type_count = sum([has_string, has_number, has_list, has_dict, has_bool, has_none])
if type_count > 1:
report['variables'][var_name]['potential_type_changes'] = True
report['statistics']['type_changed_variables'] += 1
report['potential_issues'].append(
f"Variable '{var_name}' appears to change types"
)
# Check for variables that might be unused
var_usage_pattern = re.compile(r'(?= 4.0:
performance_bonus = base_salary * 0.1
elif performance_rating >= 3.0:
performance_bonus = base_salary * 0.05
else:
performance_bonus = 0
# Loyalty bonus based on years of service
loyalty_factor = years_service // 5
loyalty_bonus = loyalty_factor * 1000
# Total bonus
total_bonus = performance_bonus + loyalty_bonus
# Convert to string for reporting
performance_bonus = f"${performance_bonus:.2f}"
loyalty_bonus = f"${loyalty_bonus:.2f}"
total_bonus = f"${total_bonus}" # Oops, this will cause an error!
# Unused variable
report_date = "2025-05-13"
print(f"Employee {employee_id} receives a total bonus of {total_bonus}")
"""
profile_result = profile_script(sample_script)
print("Script Profile:")
print(f"Total variables: {profile_result['statistics']['total_variables']}")
print(f"Reassigned variables: {profile_result['statistics']['reassigned_variables']}")
print(f"Variables changing type: {profile_result['statistics']['type_changed_variables']}")
print(f"Unused variables: {profile_result['statistics']['unused_variables']}")
print(f"None assignments: {profile_result['statistics']['none_assignments']}")
print("\nPotential issues:")
for issue in profile_result['potential_issues']:
print(f"- {issue}")
print("\nVariable assignment details:")
for var_name, details in profile_result['variables'].items():
print(f"- {var_name}: {len(details['assignments'])} assignments")
if details.get('potential_type_changes', False):
print(f" Warning: Type appears to change")
print(f" Assignments: {details['assignments']}")
Even experienced Python developers can stumble over these common variable-related issues:
One of Python's most notorious gotchas involves mutable default arguments:
# Problematic function with mutable default argument
def add_to_list(item, my_list=[]):
my_list.append(item)
return my_list
print(add_to_list("a")) # ['a']
print(add_to_list("b")) # ['a', 'b'] - Surprise! The list persisted
# Correct pattern
def add_to_list_correctly(item, my_list=None):
if my_list is None:
my_list = []
my_list.append(item)
return my_list
print(add_to_list_correctly("a")) # ['a']
print(add_to_list_correctly("b")) # ['b'] - Each call gets a fresh list
Python's scoping rules can sometimes cause confusion:
# Global vs. local scope
counter = 0
def increment():
counter = counter + 1 # This will fail
return counter
try:
print(increment())
except UnboundLocalError as e:
print(f"Error: {e}") # Error: local variable 'counter' referenced before assignment
# Correct version using 'global'
counter = 0
def increment_correctly():
global counter
counter = counter + 1
return counter
print(increment_correctly()) # 1
print(counter) # 1 (global variable was modified)
When working with mutable objects, understanding the difference between shallow and deep copies is crucial:
import copy
# Original list with nested structure
original = [1, [2, 3], 4]
# Simple assignment (reference)
reference = original
reference[0] = 99
print(original) # [99, [2, 3], 4] - Original also changed
# Reset
original = [1, [2, 3], 4]
# Shallow copy
shallow = original.copy() # or list(original) or original[:]
shallow[0] = 99 # Modifying a top-level element
print(original) # [1, [2, 3], 4] - Original unchanged
shallow[1][0] = 99 # Modifying a nested element
print(original) # [1, [99, 3], 4] - Original IS changed
# Reset
original = [1, [2, 3], 4]
# Deep copy
deep = copy.deepcopy(original)
deep[1][0] = 99
print(original) # [1, [2, 3], 4] - Original unchanged
For those ready to dive deeper, here are some advanced concepts related to Python variables:
Understanding how Python manages memory can help you write more efficient code:
# Check the memory address of a variable
a = 42
print(f"Memory address of a: {id(a)}")
# Small integers share the same object (implementation dependent)
b = 42
print(f"Memory address of b: {id(b)}")
print(f"Are a and b the same object? {a is b}") # Often True for small integers
# But larger or calculated numbers might not
large_a = 1000000
large_b = 1000000
print(f"Are large_a and large_b the same object? {large_a is large_b}") # Often False
# String interning is also implementation-dependent
str_a = "hello"
str_b = "hello"
print(f"Are str_a and str_b the same object? {str_a is str_b}") # Often True for simple strings
# But larger or dynamic strings might not be interned
str_c = "hello" * 1000
str_d = "hello" * 1000
print(f"Are str_c and str_d the same object? {str_c is str_d}") # Often False
Python 3.5+ introduced optional type hints, which can improve code clarity and enable static type checking:
from typing import List, Dict, Optional, Union, Callable
# Function with type hints
def process_user_data(
user_id: int,
name: str,
age: Optional[int] = None,
roles: List[str] = None,
metadata: Dict[str, Union[str, int, bool]] = None,
callback: Callable[[bool], None] = None
) -> Dict[str, Union[int, str, list]]:
"""Process user data with type-annotated parameters."""
result = {
'id': user_id,
'name': name,
'processed': True
}
if age is not None:
result['age'] = age
if roles:
result['roles'] = roles
if metadata:
result['metadata'] = {k: v for k, v in metadata.items() if v is not None}
if callback:
callback(True)
return result
# Usage
result = process_user_data(
user_id=1001,
name="Alex Johnson",
age=34,
roles=["admin", "editor"],
metadata={"last_login": "2025-05-10", "posts_count": 27, "verified": True}
)
print(result)
Type hints don't change how Python runs your code—they're just annotations. Tools like mypy can perform static type checking to catch potential errors before runtime.
For advanced use cases, Python 3.7 introduced context variables for managing state within a context:
import contextvars
import asyncio
# Define a context variable
request_id = contextvars.ContextVar('request_id', default=None)
async def process_request(req_id):
# Set the request ID for this context
token = request_id.set(req_id)
try:
# The request ID is now available to any function in this context
await handle_request()
finally:
# Reset the request ID
request_id.reset(token)
async def handle_request():
# Get the current request ID
current_id = request_id.get()
print(f"Handling request: {current_id}")
# Simulate some work
await asyncio.sleep(0.1)
# Log with the request ID
await log_action(f"Processed request {current_id}")
async def log_action(message):
# Access the current request ID even though it wasn't passed as an argument
current_id = request_id.get()
print(f"[Request {current_id}] {message}")
async def main():
# Process multiple requests concurrently
await asyncio.gather(
process_request('REQ-001'),
process_request('REQ-002'),
process_request('REQ-003')
)
# Run the example
asyncio.run(main())
Context variables are particularly useful in asynchronous code where thread-local storage won't work correctly.
Congratulations! You've covered a comprehensive exploration of Python variables and their capabilities:
None as Python's representation of "no value"These foundational skills provide you with the tools to write more elegant, flexible, and robust Python code. As you continue your Python journey, you'll find that mastering variables and expressions is key to unlocking the full power of the language.
Want to deepen your understanding even further? Consider exploring these related topics:
__slots__ optimization for classesHappy coding, and remember: in Python, your variables are only as flexible as your imagination!