Every name in Python lives somewhere. Knowing where saves you from a whole category of confusing bugs.
Local and global
Names created inside a function are local to it: they exist while the call runs and then vanish.
def compute():
secret = 42 # local to this call
return secret
print(compute())
print(secret) # NameError - it never existed out hereFunctions can read names from the surrounding module:
tax_rate = 0.2 # module level - a global
def with_tax(amount):
return amount * (1 + tax_rate) # reading is fine
print(with_tax(100))But assigning to that name inside the function creates a new local instead of changing the global:
counter = 0
def increment_broken():
counter = counter + 1 # UnboundLocalError
return counter
print(increment_broken())Python decides at compile time that counter is local (because the function
assigns to it), then finds it has no value yet. The global keyword overrides
that:
counter = 0
def increment():
global counter
counter += 1
return counter
print(increment(), increment(), increment())global is almost always the wrong answer
A function that changes module state can break any other part of the program, and makes tests order-dependent. Pass values in, return values out. If several functions need shared state, that is usually a signal to write a class — which is Day 6.
The LEGB rule
When Python meets a name it looks in four places, in order: Local, then any Enclosing function, then Global (module), then Built-in.
name = "global"
def outer():
name = "enclosing"
def inner():
name = "local"
print("inner sees:", name)
inner()
print("outer sees:", name)
outer()
print("module sees:", name)
print("built-in example:", len("abc")) # len comes from the builtins layerDo not shadow the built-ins
Naming a variable list, sum, type or id hides the built-in for the rest
of the scope. list = [1, 2] then list("abc") fails with a baffling message.
Lambdas
A lambda is a function written as a single expression, with no name. It is
the same idea as def, minus the ceremony.
def double(x):
return x * 2
double_lambda = lambda x: x * 2
print(double(5), double_lambda(5))
# Lambdas take several arguments too:
area = lambda width, height: width * height
print(area(3, 4))Assigning a lambda to a name, as above, is pointless — def is clearer. The
real use is passing a small function to another function.
Sorting with key=
This is where lambdas earn their existence.
people = [
("Ada", 1815, "Mathematician"),
("Grace", 1906, "Admiral"),
("Alan", 1912, "Logician"),
]
print(sorted(people, key=lambda person: person[1])) # by year
print(sorted(people, key=lambda person: person[0])) # by name
print(sorted(people, key=lambda person: len(person[2]))) # by role length
words = ["banana", "Fig", "cherry", "apple"]
print(sorted(words)) # capitals sort first
print(sorted(words, key=str.lower)) # case-insensitiveThe key function is called once per item, and Python sorts by whatever it
returns. The same idea works for max, min and sorted alike:
books = [
{"title": "Dune", "pages": 412},
{"title": "Piranesi", "pages": 245},
{"title": "Ubik", "pages": 224},
]
longest = max(books, key=lambda book: book["pages"])
print("Longest:", longest["title"])
by_title = sorted(books, key=lambda book: book["title"])
for book in by_title:
print(f" {book['title']} ({book['pages']}p)")Sort a leaderboard
Sort the players by score, highest first; where scores tie, sort by name
alphabetically. Print each as 1. Ada — 95.
Hint: a key can return a tuple, and Python compares tuples item by item.
players = [
{"name": "Grace", "score": 88},
{"name": "Ada", "score": 95},
{"name": "Alan", "score": 88},
]
# Your code hereShow one solution
players = [
{"name": "Grace", "score": 88},
{"name": "Ada", "score": 95},
{"name": "Alan", "score": 88},
]
ranked = sorted(players, key=lambda p: (-p["score"], p["name"]))
for position, player in enumerate(ranked, start=1):
print(f"{position}. {player['name']} — {player['score']}")The trick is the tuple key (-score, name): negating the score sorts it
descending while the name still sorts ascending, in a single pass.
What you learned
- Names assigned in a function are local and disappear when it returns.
- Functions can read globals but assigning creates a local —
globaloverrides this, and is rarely right. - Python resolves names Local → Enclosing → Global → Built-in.
lambda args: expressionis an unnamed one-expression function.key=letssorted,maxandminsort by anything, including a tuple.