Parameters and Return Types
So far, every value you have worked with sits in a variable: title: str = "My Note", word_count: int = 350. But variables just store data. To do something with that data, you need a function.
A function is a reusable block of code with a name. You give it some inputs, it does its work, and it gives you back a result. In Chapter 46, you wrote your first function stub:
def celsius_to_fahrenheit(celsius: float) -> float: ...
That function takes one input (celsius) and gives back one output (a float). The input goes inside the parentheses. The output type goes after the -> arrow.
But what if a function needs more than one input? Think about it: to calculate reading time, you need two pieces of information, not one. You need the word count AND the reading speed. With only the word count, you cannot calculate anything.
The inputs inside the parentheses are called parameters. A function can have one parameter, two, three, or more. Each parameter has a name and a type, just like a variable. They are separated by commas.
James needs a function that calculates reading time from a word count and a reading speed. He starts typing:
def reading_time(words, speed):
Emma stops him. "What type is words?"
"An integer."
"And speed?"
"Also an integer."
"Then say so. If you do not tell pyright, it cannot help you. If you do not tell AI, it guesses."
James adds the types:
def reading_time(words: int, speed: int) -> float:
...
Now the function has a contract: it takes two integers and returns a float. Pyright can check every call, and AI knows exactly what to build.
How Python Knows What Is Inside a Function
Many programming languages use curly braces {} to mark what belongs inside a function. Python uses indentation: 4 spaces.
def greet(name: str) -> str:
return f"Hello, {name}!" # ← 4 spaces in = inside the function
print(greet("James")) # ← no indent = outside the function
Output:
Hello, James!
Everything indented 4 spaces after the def line belongs to that function. When the indentation stops, the function ends. If you forget to indent, Python raises an IndentationError before the code runs.
def greet(name: str) -> str:
return f"Hello, {name}!" # IndentationError: expected an indented block
Output:
IndentationError: expected an indented block after function definition on line 1
Python is strict about spacing. Use exactly 4 spaces for each level of indentation. Most editors (including VS Code) insert 4 spaces when you press Tab. Never mix tabs and spaces in the same file. If you see an IndentationError, check that every line inside the function starts at the same position.
Writing Multi-Parameter Signatures
A function signature lists every input the function needs, each with its type. The return type comes after ->:
def full_name(first: str, last: str) -> str:
return f"{first} {last}"
result: str = full_name("James", "Park")
print(result)
Output:
James Park
Each parameter is a promise. first: str means "this function expects text as the first argument." -> str means "this function gives text back." Pyright enforces every promise.
How to call a function
Writing def full_name(first: str, last: str) -> str: creates the function. To use it, you call it by writing the function name followed by parentheses with the values you want to pass:
full_name("James", "Park")
The values you pass in ("James" and "Park") are called arguments. They fill the parameters in order: "James" goes into first, "Park" goes into last. The function runs its body and gives back the result.
You can save the result in a variable:
result: str = full_name("James", "Park")
print(result)
Output:
James Park
Or print it directly:
print(full_name("Emma", "Chen"))
Output:
Emma Chen
More examples with different types
Here are three more functions. Each one is defined AND called, so you can see both parts:
def note_summary(title: str, word_count: int) -> str:
return f"{title} ({word_count} words)"
print(note_summary("My First Note", 350))
Output:
My First Note (350 words)
def apply_discount(price: float, percent: float) -> float:
return price * (1 - percent / 100)
print(apply_discount(9.90, 10.0))
Output:
8.91
def is_long_note(word_count: int, threshold: int) -> bool:
return word_count > threshold
print(is_long_note(500, 300))
Output:
True
Notice the pattern: every parameter has a name, a colon, and a type. The return type follows ->. The body is indented 4 spaces. And to call a function, you write its name with arguments in parentheses.
Return vs Print
These two functions look similar but do completely different things:
Function A uses return:
def calculate_total(price: float, tax: float) -> float:
return price * (1 + tax)
result: float = calculate_total(100.0, 0.08)
print(result)
print(type(result))
Output:
108.0
<class 'float'>
calculate_total hands a value back to whoever called it. You can store that value in a variable, test it, or pass it to another function. The result is a real float you can use.
Function B uses print:
def show_total(price: float, tax: float) -> None:
print(f"Total: {price * (1 + tax)}")
nothing = show_total(100.0, 0.08)
print(nothing)
Output:
Total: 108.0
None
show_total displays text on screen for a human to read, then returns None (nothing). The variable nothing holds None because print() does not give back a value. You cannot do math with None. You cannot test it. It is gone.
The difference: return gives you a value you can reuse. print() shows text on screen and gives you nothing.
Print is for people, return is for reuse. Think of return as putting a result in a box and handing it to whoever asked. print() is shouting the result out loud for everyone to hear, but nobody gets a box. When you write tests, you test what functions RETURN, not what they print.
In Python, return and print are completely separate. A function that only prints has an implicit return type of None. Annotate it as -> None to make this explicit. Pyright will flag you if you try to use the return value of a -> None function.
The Signature Is the Contract
A function signature is a contract with two audiences:
- Pyright reads the types and catches mismatches before you run anything
- AI reads the signature (and the docstring you will learn to write in this chapter's Lesson 3) to generate the body
When you write def apply_discount(price: float, percent: float) -> float:, you are saying: "This function takes two decimal numbers and gives back a decimal number." That is enough for AI to infer the body, and enough for pyright to check every call site.
# Pyright catches this mistake: wrong argument type
apply_discount("hundred", 10.0) # Error: "str" is not assignable to "float"
Output (from pyright):
error: Argument of type "str" is not assignable to parameter "price" of type "float"
Without the type annotation, pyright would not catch this. The code would crash at runtime with a TypeError instead of being caught before you run it.
PRIMM-AI+ Practice: Reading Signatures
Predict [AI-FREE]
Press Shift+Tab to enter Plan Mode before predicting.
Look at these three function signatures. There is no body, only the signature and .... From the name, parameter types, and return type alone, predict what each function does. Write your predictions and a confidence score from 1 to 5 before checking.
def character_count(text: str) -> int: ...
def combine_tags(tag_a: str, tag_b: str) -> str: ...
def reading_minutes(words: int, speed: int) -> float: ...
Check your predictions
Function 1: character_count takes a str and returns an int. The name says "character count," so it likely counts how many characters are in the text. One string in, one integer out.
Function 2: combine_tags takes two str values and returns a str. The name says "combine tags," so it likely joins two tags into a single string. Two strings in, one string out.
Function 3: reading_minutes takes two int values and returns a float. The name says "reading minutes," the parameters are words and speed, so it likely divides words by speed to get reading time. The return type is float (not int) because division in Python always returns a float (Chapter 47).
If you predicted all three correctly from the signatures alone, you are reading contracts fluently. The signature tells you what goes in, what comes out, and the name tells you what it does. That is the whole contract.
Run
Press Shift+Tab to exit Plan Mode.
Ask AI to implement all three functions. Paste these stubs and say "Implement the body of each function. Keep each body to one line." Then call each one with test values and compare the output to what you predicted.
Investigate
For reading_minutes(1500, 250), the result is 6.0 (a float, not 6). Write one sentence explaining why Python returns a float here, using what you learned in Chapter 47 about the / operator.
If you want to go deeper, run /investigate @reading_minutes.py in Claude Code and ask why Python's division operator always returns float even when both operands are integers.
Modify
Change reading_minutes to use // (floor division) instead of /. What changes about the return type? Update the annotation to match and run pyright to confirm.
Make [Mastery Gate]
Without looking at any examples, write 3 function signatures from these English descriptions. Write only the signature and ... (no body):
- A function that takes a note title (text) and a tag (text) and returns a formatted string
- A function that takes a price (decimal) and a quantity (whole number) and returns the total (decimal)
- A function that takes a word count (whole number) and a limit (whole number) and returns whether the count exceeds the limit (true/false)
Run uv run pyright on your file. Zero errors means you pass.
Try With AI
If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.
Prompt 1: Generate a Function from Your Signature
Here is a function signature I wrote:
def note_reading_time(word_count: int, words_per_minute: int) -> float:
...
Implement the body. Keep it to one line. Do not use any
if-statements or loops.
Read AI's implementation. Does it match what you expected from the signature? Does it use / (returning float) or // (returning int)? If it uses //, the return type annotation says float but the body returns an int. Tell AI about the mismatch and ask it to fix.
What you're learning: The signature is your specification. When AI's implementation does not match your return type, you catch it the same way pyright does: by checking the contract.
Prompt 2: Spot the Return vs Print Mistake
I wrote this function:
def format_price(amount: float, currency: str) -> str:
print(f"{currency}{amount:.2f}")
My test says: assert format_price(9.99, "$") == "$9.99"
But the test fails with "AssertionError: None != '$9.99'"
What is wrong?
Read AI's explanation. It should identify that print() returns None instead of the formatted string. The fix is to replace print(...) with return .... This is the "print is for people, return is for reuse" principle in action.
What you're learning: You are diagnosing a common beginner mistake by presenting it to AI and reading the explanation. The pattern is: state your confusion, let AI explain, then verify the explanation matches what you learned.
James looks at his four SmartNotes functions. "Every parameter has a name, a type, and a purpose. Every function has a return type. And I did not write a single function body myself."
"You wrote the contracts," Emma says. "The bodies followed from the contracts. That is the whole point."
James pauses. "But every parameter is required right now. What if reading speed is usually 250 words per minute? Do I have to type 250 every single time I call the function?"
"No. That is what default values are for. Lesson 2. You give a parameter a default, and callers can skip it when the default is what they want."