A function is a named block of code that does one job. You write it once, then call it from anywhere. Functions are how programs grow without becoming unreadable.
Defining a function
def greet(name: str) -> str:
return f"Hello, {name}!"
Four pieces:
def— the keyword that starts a function definition.greet(name: str)— the function’s name (greet), followed by its parameters in parentheses. Each parameter has a name and a type hint.-> str— the return type hint. This function returns astr.- The colon ends the line; the body is indented below.
Calling a function
message: str = greet("Manikandan")
print(message)
Hello, Manikandan!
When you call a function, Python:
- Takes the values you passed (
"Manikandan") and binds them to the parameter names (name). - Runs the function body.
- Hands back whatever the function
returns.
Type hints — required in this course
A function definition without type hints looks like this:
def greet(name):
return f"Hello, {name}!"
This works. Python ignores types at runtime. But we’ll always write them, because:
- The type checker (
pyright) can catch mistakes before the code runs. - Other people reading the code know what to pass in and what they’ll get back.
- Your editor can autocomplete based on types.
Build the habit from day one. Untyped Python is harder to maintain, even for the person who wrote it.
Return values
A function can return any value:
def add(a: int, b: int) -> int:
return a + b
def is_even(n: int) -> bool:
return n % 2 == 0
def double(items: list[int]) -> list[int]:
return [n * 2 for n in items]
The return statement does two things: it sends a value back, and it immediately exits the function. Any code after a return won’t run:
def absolute(x: int) -> int:
if x < 0:
return -x
return x # only runs if x was 0 or positive
print("never") # unreachable
Functions that don’t return a value
If a function doesn’t return anything, its return type is None. The return statement is optional:
def greet(name: str) -> None:
print(f"Hello, {name}!")
# no return needed
greet("World")
You can also write a bare return to exit early:
def safe_divide(a: float, b: float) -> None:
if b == 0:
print("Cannot divide by zero")
return # exits the function early
print(f"{a} / {b} = {a / b}")
Multiple parameters
def rectangle_area(width: float, height: float) -> float:
return width * height
area: float = rectangle_area(3.0, 5.0)
print(area) # 15.0
Parameters are matched in order — the first value goes to width, the second to height.
Functions can call other functions
This is the whole point — small functions that call each other:
def square(x: float) -> float:
return x * x
def hypotenuse(a: float, b: float) -> float:
return (square(a) + square(b)) ** 0.5
print(hypotenuse(3, 4)) # 5.0
A small refactor — before and after
Without functions:
# main.py
import math
x1, y1 = 1.0, 2.0
x2, y2 = 4.0, 6.0
distance_1 = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
x1, y1 = 0.0, 0.0
x2, y2 = 3.0, 4.0
distance_2 = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
With a function:
# main.py
import math
def distance(x1: float, y1: float, x2: float, y2: float) -> float:
return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
print(distance(1, 2, 4, 6))
print(distance(0, 0, 3, 4))
The function version reads in one line, can’t get out of sync with itself, and would work just as well with 100 calls as with 2.
Naming
Functions follow the same naming conventions as variables — snake_case, lowercase, descriptive:
def parse_user_input(...): # good
def parse_input(...): # OK
def parseInput(...): # wrong — that's JavaScript style
def x(...): # bad — too short
A function name should answer “what does this do?”. Good names: load_config, is_valid_email, compute_average. Bad names: do_it, process, helper.
What’s next
You can define functions and call them. Next, we’ll see Python’s flexible argument passing — keyword arguments and default values.