Intermediate
12 min read
#Python#Functions#Scope#Closures#Lambda

Python Functions — The Complete Notebook

Comprehensive guide on Python Functions — The Complete Notebook.

Python Functions

1. Overview#

Functions are the first level of abstraction in Python — a way to name a piece of behavior so it can be reused, tested, and reasoned about independently. This note covers function anatomy, scope rules, closures, and functions as first-class objects. (Decorators — functions that wrap other functions — get their own dedicated note: decorators.md.)


2. Defining Functions#

🐍 Python
def greet(name): """Return a friendly greeting for the given name.""" return f"Hello, {name}!" print(greet("Kamal")) print(greet.__doc__) # Docstrings are accessible at runtime — useful for tooling

2.1 Type Hints#

Type hints don't change runtime behavior, but they make intent explicit and enable static analysis tools like mypy.

🐍 Python
def calculate_total(price: float, quantity: int, discount: float = 0.0) -> float: return (price * quantity) * (1 - discount) total: float = calculate_total(499.0, 3, discount=0.1)

3. Argument Types in Depth#

🐍 Python
def build_request(url, method="GET", *args, timeout=30, **headers): print(f"URL: {url}") print(f"Method: {method}") print(f"Extra positional args: {args}") print(f"Timeout: {timeout}") print(f"Headers: {headers}") build_request( "https://api.example.com", "POST", "extra1", "extra2", timeout=10, Authorization="Bearer token123" )
SyntaxNameBehavior
def f(a, b)Positional-or-keywordCan be passed either way
def f(a=1)Default argumentUsed if caller omits the value
def f(*args)Variadic positionalCollects extras into a tuple
def f(**kwargs)Variadic keywordCollects extras into a dict
def f(a, /, b)Positional-onlya cannot be passed as a=value (3.8+)
def f(*, b)Keyword-onlyb must be passed as b=value
🐍 Python
def move_point(x, y, /, *, label): return f"{label}: ({x}, {y})" move_point(3, 4, label="origin") # OK # move_point(x=3, y=4, label="origin") # TypeError — x, y are positional-only

3.1 Unpacking Arguments When Calling#

🐍 Python
def add(a, b, c): return a + b + c values = [1, 2, 3] print(add(*values)) # unpack list into positional args kwargs = {"a": 1, "b": 2, "c": 3} print(add(**kwargs)) # unpack dict into keyword args

4. Return Values & Unpacking#

🐍 Python
def min_max(numbers): return min(numbers), max(numbers) # returns a tuple lowest, highest = min_max([4, 8, 15, 16, 23, 42]) print(lowest, highest) # 4 42
🐍 Python
def get_stats(numbers): return { "count": len(numbers), "sum": sum(numbers), "avg": sum(numbers) / len(numbers), } stats = get_stats([10, 20, 30]) print(stats["avg"]) # 20.0

5. Scope: The LEGB Rule#

Python resolves a variable name by checking scopes in this order: Local → Enclosing → Global → Built-in.

🐍 Python
x = "global x" def outer(): x = "enclosing x" def inner(): x = "local x" print(x) # local x — Local scope wins first inner() print(x) # enclosing x outer() print(x) # global x

5.1 global and nonlocal#

🐍 Python
counter = 0 def increment(): global counter counter += 1 increment() increment() print(counter) # 2 def make_counter(): count = 0 def increment(): nonlocal count # modifies the enclosing variable, not global count += 1 return count return increment counter_fn = make_counter() print(counter_fn()) # 1 print(counter_fn()) # 2

Reaching for global is usually a sign the design could be improved — passing state explicitly or using a class is often clearer and safer in larger programs.


6. Closures#

A closure is a function that "remembers" variables from the scope it was created in, even after that scope has finished executing.

🐍 Python
def make_multiplier(factor): def multiplier(value): return value * factor # factor is "closed over" return multiplier double = make_multiplier(2) triple = make_multiplier(3) print(double(10)) # 20 print(triple(10)) # 30 print(double.__closure__[0].cell_contents) # 2

Practical use case — configuration factories:

🐍 Python
def make_validator(min_val, max_val): def validate(value): return min_val <= value <= max_val return validate is_valid_age = make_validator(0, 120) print(is_valid_age(25)) # True print(is_valid_age(200)) # False

7. Lambda Functions#

Anonymous, single-expression functions — best for short, throwaway logic passed to another function.

🐍 Python
employees = [ {"name": "Asha", "salary": 72000}, {"name": "Ravi", "salary": 65000}, {"name": "Meera", "salary": 81000}, ] top_earners = sorted(employees, key=lambda e: e["salary"], reverse=True) print(top_earners[0]["name"]) # Meera

If a lambda needs more than one line of logic or a name to explain itself, write a regular def function instead — lambdas should stay trivial.


8. Functions as First-Class Objects#

Functions in Python are objects — they can be assigned to variables, stored in data structures, and passed around like any other value.

🐍 Python
def celsius_to_fahrenheit(c): return c * 9 / 5 + 32 def fahrenheit_to_celsius(f): return (f - 32) * 5 / 9 converters = { "c_to_f": celsius_to_fahrenheit, "f_to_c": fahrenheit_to_celsius, } print(converters["c_to_f"](100)) # 212.0

8.1 Higher-Order Functions: map, filter, functools.reduce#

🐍 Python
from functools import reduce numbers = [1, 2, 3, 4, 5] squared = list(map(lambda n: n ** 2, numbers)) evens = list(filter(lambda n: n % 2 == 0, numbers)) total = reduce(lambda acc, n: acc + n, numbers, 0) print(squared) # [1, 4, 9, 16, 25] print(evens) # [2, 4] print(total) # 15

A list comprehension is usually more Pythonic than map/filter for simple cases: [n ** 2 for n in numbers] reads more naturally than map(lambda n: n ** 2, numbers).


9. Recursion#

🐍 Python
def factorial(n): if n <= 1: # base case — stops the recursion return 1 return n * factorial(n - 1) # recursive case print(factorial(5)) # 120
🐍 Python
def fibonacci(n, memo={}): if n in memo: return memo[n] if n <= 1: return n memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo) return memo[n] print(fibonacci(30)) # fast, thanks to memoization

Python has a default recursion limit (sys.getrecursionlimit(), usually 1000). Deep recursion in Python is often less efficient than an equivalent loop — use recursion where it makes the logic clearer, not by default.


10. Generators & yield#

Generators produce values lazily, one at a time, instead of building an entire list in memory — essential for large or infinite data streams.

🐍 Python
def read_large_file_lines(filepath): with open(filepath) as f: for line in f: yield line.strip() def fibonacci_sequence(): a, b = 0, 1 while True: yield a a, b = b, a + b fib = fibonacci_sequence() first_10 = [next(fib) for _ in range(10)] print(first_10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
FeatureListGenerator
MemoryStores all items at onceProduces one item at a time
ReusableYes, iterate repeatedlyNo, exhausted after one pass
Use caseSmall/medium datasetsStreaming, large/infinite data

10.1 Generator Expressions#

🐍 Python
squares = (n ** 2 for n in range(1_000_000)) # lazy — no memory spike print(next(squares)) # 0 print(sum(squares)) # sums the rest without ever building a full list

11. Summary & Best Practices Checklist#

  • Use type hints on functions in shared/production code — they double as documentation.
  • Keep functions small and focused on one responsibility.
  • Avoid global; prefer passing state explicitly or using closures/classes.
  • Reach for a closure when you need a function "pre-configured" with some state.
  • Keep lambdas to single, trivial expressions — use def otherwise.
  • Prefer comprehensions over map/filter for simple transformations.
  • Use generators instead of lists when working with large or streaming data.
  • Always define a clear base case before writing recursive logic.
Knowledge Checkpoint

Functions, Closures & LEGB Scope Checkpoint

Q1.In Python's LEGB scope lookup order, what does the 'E' stand for?
AExternal
BEnclosing
CEnvironment
DExported
Q2.Which keyword must you use inside an inner nested function to reassign a variable defined in the outer function's scope?
Aglobal
Bnonlocal
Couter
Dparent
Q3.What is captured inside a Python function closure when an inner function accesses an outer variable?
AA static frozen copy of the value at the moment of function creation.
BA cell object reference that dynamically reflects updates to the variable in the outer scope.
CA deep copy of the global environment.
DThe compiled bytecode of the outer function.
Q4.What do `*args` and `**kwargs` pack positional and keyword parameters into, respectively?
A`*args` into a list, `**kwargs` into a dictionary
B`*args` into a tuple, `**kwargs` into a dictionary
C`*args` into a set, `**kwargs` into a tuple
D`*args` into a generator, `**kwargs` into a JSON string
Track Your Learning

Finished studying this notebook?

Mark this guide as completed to update your course progress roadmap.