Chapter 10 of 24 · Python for Beginners — Complete Free Tutorial
Functions
A function is a named block of code you can run as many times as you like, with different inputs each time. Everything you have built so far in the Python for Beginners series has been straight-line code; functions are the step that turns a script into a program. This chapter covers defining and calling, the four kinds of parameter, returning values (including several at once), scope, and the small documentation toolkit — docstrings, type hints and lambda.
Defining and calling
def introduces a function, the indented block is its body, and return hands a value back — without one the function still answers, the special value None. Nothing runs until the name is called, so definitions must appear before their first use. Returning several results at once costs nothing: write them as a comma-separated tuple and unpack at the call site (the topic of chapter 8):
def shout(text):
return text.upper()
def min_max(numbers):
return min(numbers), max(numbers)
print(shout("hello"))
lo, hi = min_max([4, 1, 9, 2])
print(lo, hi)
Expected output:
HELLO
1 9
Parameters: positional, keyword and defaults
Arguments can arrive by position, by name, or be left out entirely when you gave the parameter a default. Mixing is allowed as long as positionals come first — greet("Ada") matches by position, greet("Grace", greeting="Hi") matches by name, and too few or too many positionals raise a TypeError:
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Ada"))
print(greet("Grace", greeting="Hi"))
print(greet(greeting="Yo", name="Lin"))
Expected output:
Hello, Ada!
Hi, Grace!
Yo, Lin!
Callers who know JavaScript will recognise these as the same ideas as ordinary JS functions, except that a default value is evaluated once, when the def runs — never use a mutable default such as items=[]. Defaults must also come after required parameters in the signature.
Collecting extra arguments: *args and **kwargs
Two marker parameters let a function accept any number of arguments. *args gathers surplus positionals into a tuple; **kwargs gathers surplus keywords into a dictionary:
def total(*prices):
print(type(prices).__name__, prices)
return sum(prices)
print(total(4, 5, 6))
def build_profile(first, last, **details):
profile = {"first": first, "last": last}
profile.update(details)
return profile
print(build_profile("Ada", "Lovelace", role="mathematician", year=1843))
Expected output:
tuple (4, 5, 6)
15
{'first': 'Ada', 'last': 'Lovelace', 'role': 'mathematician', 'year': 1843}
The names args and kwargs are conventions, not syntax — *prices and **options work identically. The stars mean the opposite at a call site: f(*items, **config) unpacks a sequence and a mapping into arguments.
Scope: where a name lives
Python searches for names in four layers — Local, Enclosing, Global, Builtin — in that order, which every Python programmer abbreviates as LEGB. The first place a matching name is found wins:
level = "global"
def outer():
level = "enclosing"
def inner():
level = "local"
print("inner sees:", level)
inner()
print("outer sees:", level)
outer()
print("module sees:", level)
Expected output:
inner sees: local
outer sees: enclosing
module sees: global
Each function gets its own local namespace, and reading an outer name never modifies it. To write to a global you must say so with global counter — and you should do so rarely: the statement makes the function depend on state defined elsewhere, the same hazard JavaScript has with undeclared variables. Prefer returning a value, or keep the state in a class when chapter 16 arrives. nonlocal is the sibling keyword for rebinding a name in an enclosing function, a common companion to closures such as the ones explored in scope, hoisting and closures.
Docstrings and type hints
Two habits document a function for the next reader. A docstring is a string literal as the first statement of the body — as long as you like, and shown by help() and every editor. Type hints are optional annotations describing the shapes involved; Python ignores them while running, so they exist for readers and for static checkers such as mypy:
def circle_area(radius: float) -> float:
"""Return the area of a circle with the given radius."""
return 3.14159 * radius ** 2
def add(a: int, b: int) -> int:
return a + b
print(circle_area.__doc__.splitlines()[0])
print(add(2, 3))
Expected output:
Return the area of a circle with the given radius.
5
Nothing checks the hints at runtime — call add("a", "b") and it happily concatenates two strings, annotations and all. Type hints get a dedicated chapter in chapter 20, and readers who know functions in TypeScript will find the notation familiar.
Lambdas and first-class functions
In Python, functions are ordinary values: you can pass them, store them and return them — announce("hello", shout) below hands one function to another as an argument. Anywhere an expression is allowed, a small anonymous lambda function can stand in:
def shout(text):
return text.upper()
def announce(text, transform):
return transform(text)
print(announce("hello", shout))
students = [("Ada", 91), ("Alan", 72), ("Grace", 88)]
students.sort(key=lambda row: row[1], reverse=True)
print(students)
Expected output:
HELLO
[('Ada', 91), ('Grace', 88), ('Alan', 72)]
Lambdas have hard limits: one expression, no statements, no docstring, hard to test in isolation. The style rule is simple — use a lambda for a short key or callback you will never name again, and write a real def for anything with a branch in it. When you see a function being handed to another function, think of array methods such as map and filter from arrays and array methods; the pattern is the same, only the plumbing differs.
A worked program: a shipping calculator
Everything in this chapter in one practical program — required parameters, an optional keyword with a default, a docstring, and a loop that calls the function per parcel:
def shipping_cost(weight_kg, distance_km, express=False):
"""Return the delivery cost for one parcel, rounded to cents."""
base = 4.5 + 1.25 * weight_kg
per_km = 0.03 if express else 0.015
return round(base + distance_km * per_km, 2)
parcels = [("book", 0.8, 12, False), ("laptop", 2.4, 900, True)]
for name, weight, km, express in parcels:
cost = shipping_cost(weight, km, express=express)
mode = "express" if express else "standard"
print(f"{name}: ${cost:.2f} ({mode})")
Expected output:
book: $5.68 (standard)
laptop: $34.50 (express)
Each parcel tuple is unpacked into four names, the function does the arithmetic once, and the caller only formats. That separation — compute here, present there — is the whole point of functions.
Common mistakes
- Calling before defining — a function only exists from the moment its
defexecutes; move the call underneath or the definition above. - Mutable default arguments —
def add(item, bucket=[])shares one list across every call; default toNoneand create the list inside. - Forgetting
return— the function answersNone, and assigning its result silently poisons the variable withNone. - Overusing
global— hidden shared state makes functions depend on execution order; return values instead. - Reaching for
lambdatoo early — a multi-branch anonymous function is unreadable; give it a name and adef.
Practice checklist
- Write a
describe(name, role="student")function and call it four ways: positionally, with a keyword, with both, and with neither. - Write a function that returns three values in one
returnand unpack them into three variables. - Add a docstring to each function you have written so far and read one back with
help(). - Reproduce the LEGB example, then change
innerto printlevelwithout assigning it and observe which value wins. - Convert one of your earlier loops into a function with parameters, and confirm the output is unchanged.
What's next?
Functions let you name a behaviour — next you can name a whole transformation in a single line. Chapter 11 introduces List, Dict and Set Comprehensions.
Frequently Asked Questions
Python is a general-purpose language first released in 1991, designed around one idea: code is read far more often than it is written. English keywords and indentation instead of braces make it readable, the standard library covers files, JSON, dates and testing, and pip adds thousands more packages. The same language handles scripts, web APIs, data work and machine learning.
None at all. Python is widely considered the friendliest first language: no declaration keywords, no braces, no build step, and error messages that name the problem. This series begins at your first print() call. If you already read JavaScript or TypeScript you will adjust quickly, but no previous language is assumed anywhere in these 24 chapters.
Python 3 only, any release from 3.12 onwards. Python 2 reached end of life in 2021 and its print "hi" statements and xrange() no longer run. One historical quirk remains: installers ship the interpreter as python3 on macOS and Linux, while Windows usually offers python or the py launcher, and every command in this series works with that one-word swap.
More than you might expect. Everyday automation such as renaming files, cleaning spreadsheets or scraping pages; web backends with Django, Flask or FastAPI; data analysis and machine learning with pandas, NumPy and PyTorch; DevOps tooling, desktop apps and games. Python is also the classic glue language, calling C, Rust or Fortran libraries from short readable scripts.
Pure Python can run 10 to 100 times slower than C, yet that rarely blocks you: heavy numeric work lives in C extensions such as NumPy, which Python coordinates. Dynamic typing does push errors to runtime, surfacing as a TypeError on the line that finally uses the bad value. Type hints checked by mypy move those mistakes back into your editor.
Only the interpreter. Install CPython from python.org, version 3.12 or newer, confirm it with python3 --version, and open a plain text editor; VS Code with the Python extension is the recommendation here. The early chapters use no third-party packages, so there is nothing else to set up until virtual environments and pip arrive.
A dict is a real runtime mapping: keys may be any hashable value rather than only strings, insertion order has been guaranteed since Python 3.7, and a missing key raises KeyError unless you ask with get(). JavaScript objects coerce every key to a string and carry a prototype chain. A TypeScript interface is erased at compile time and describes shape only; TypedDict is Python's checked equivalent.
Reach for a list when items will be added, removed or reordered: it is ordered, mutable and rich with methods. Pick a tuple for fixed records and function returns, because it is immutable and hashable, so it can be a dictionary key. Choose a set for unique values and fast membership checks; it has no order and de-duplicates on creation.
Yes, gradually. Annotate public function signatures first; the interpreter ignores hints at runtime, so a checker such as mypy is what gives them teeth. Add tests the same way: pytest collects any test_*.py file and plain assert statements, and a suite that finishes in a tenth of a second is one you will run after every change. Both habits cost little now and much more later.
JavaScript and TypeScript keep the browser, so anything rendered to the user stays theirs. Python takes the server: Django for full-featured applications, Flask for small ones, FastAPI for typed async APIs where your annotations become automatic documentation. Your TypeScript habits transfer directly, and asyncio mirrors the event loop you already know, just with different keywords.