Python: Functions, def, Parameters, Return Values

You copy five lines of code, paste them into a second spot, then a third, and a week later you fix a bug in one copy and forget the other two. Python functions kill exactly that problem: write a block of logic once, give it a name, and call it wherever you need it. This tutorial covers def syntax, parameters, return values, docstrings, imports, and the call stack, with every example tested.

“Functions should do one thing. They should do it well. They should do it only.”

Robert C. Martin, Clean Code

Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Beginner | Reading Time: 16 minutes

Think about the speed-dial buttons on a phone. You set one up once, give it a name like “Mom,” and from then on you press a single button instead of typing the full number every time. A Python function is the same idea for code. You write a block of logic once, give it a name, and then call that name whenever you need the work done.

Up to this point, you have been writing code that runs from top to bottom in one file. That is fine for a 30-line script. It falls apart the moment you hit 100 lines, because the same logic ends up copied in three places, and fixing one copy means hunting down the other two. Python functions exist to remove exactly that pain.

Functions solve that. A function is a named block of code that does one thing. You define it once, call it as many times as you want, and change it in a single place when the logic needs updating. Functions are how you go from writing “scripts” to writing “programs.”

Defining and Calling Functions

Python functions have two parts: defining one (writing the recipe) and calling it (telling Python to cook). Here is the picture first, then the simplest possible example.

1. calls greet2. calls format_name3. returns RAHUL4. returns Hello,RAHUL!Global Framename = ‘Rahul’result = greet namegreet name framename = ‘Rahul’formatted = format_namenamereturn Hello formattedformat_name name framename = ‘Rahul’return name.upperEach function call createsa new frame with its ownlocal variables. Frames aredestroyed when the functionreturns.Python Functions: How the Call Stack Builds Frames and Returns Values

The diagram shows Python’s call stack in action. When one function calls another, Python creates a fresh frame on the stack, runs it, and hands the return value back to the frame that called it. Think of a stack of plates: the newest call sits on top, and Python always finishes the top one before going back down. Each frame keeps its own local variables, which is why a variable inside one function never trips over a variable in another. This stack model is the foundation for scope, recursion, and reading those tracebacks when something breaks.

📄 basic_function.py: the def keyword, the function body, and calling

def greet():
    print("Hello from TechnoScripts!")

# Call it
greet()
greet()  # Call as many times as you want

# Function with a parameter
def greet_person(name):
    print(f"Hello, {name}! Welcome to Python.")

greet_person("Rahul")
greet_person("Aditi")

▶ Output

Hello from TechnoScripts!
Hello from TechnoScripts!
Hello, Rahul! Welcome to Python.
Hello, Aditi! Welcome to Python.

What happened here: The def keyword defines a function. The name follows the usual Python rules (lowercase with underscores). The parentheses hold the parameters, or stay empty if there are none. The indented lines underneath are the body. To run that body, you call the function by name with () after it. Leave the parentheses off and you only point at the function object, like reading “Mom” in your contacts without actually pressing call. Notice greet() ran twice and printed twice, which is the whole point: define once, reuse forever. The second function then greeted two learners, Rahul and Aditi, from the same three lines of code, just with different inputs.

Parameters and Arguments

A function with no inputs can only ever do one fixed thing. Parameters are how you pass data in so the same function can work on different values. A default value is a sensible fallback for when the caller does not bother to supply one. In the example below, we print profile cards for a few developers on a team: Viraj, Pravin, Anvi, and Vinay.

📄 parameters.py: required, optional, and multiple parameters

# Multiple parameters
def introduce(name, age, role):
    print(f"{name}, age {age}, works as {role}")

introduce("Viraj", 26, "Frontend Dev")

# Parameters with default values
def create_profile(name, city="Mumbai", lang="Python"):
    print(f"{name} from {city}, codes in {lang}")

create_profile("Pravin")                    # Uses both defaults
create_profile("Anvi", "Pune")              # Overrides city
create_profile("Vinay", lang="JavaScript")  # Keyword argument

▶ Output

Viraj, age 26, works as Frontend Dev
Pravin from Mumbai, codes in Python
Anvi from Pune, codes in Python
Vinay from Mumbai, codes in JavaScript

What happened here: Parameters are the variables listed in the function definition, like name and city. Arguments are the real values you hand over when you call it, like "Pravin" and "Pune". A simple way to remember it: parameters are the empty seats on a bus, arguments are the people who actually sit down. Default values let a caller skip a seat, and keyword arguments such as lang="JavaScript" let you fill a seat by name so order does not matter.

Return Values

Printing shows something on screen and then forgets it. Returning hands a value back to your code so you can keep working with it. That difference is the single most important idea in this whole post, so watch the return keyword closely here.

📄 returns.py: functions that give back a result

def square(n):
    return n ** 2

result = square(7)
print(f"7 squared: {result}")

# Use return value directly in expressions
total = square(3) + square(4)
print(f"3² + 4² = {total}")

def is_passing(score, threshold=60):
    return score >= threshold

print(f"85 passing: {is_passing(85)}")
print(f"45 passing: {is_passing(45)}")

▶ Output

7 squared: 49
3² + 4² = 25
85 passing: True
45 passing: False

What happened here: return ends the function on the spot and sends a value back to whoever called it. A function with no return (or just a bare return on its own) hands back None. Once a value comes back, it is yours to do anything with: store it in a variable like result, drop it straight into a bigger expression as we did with square(3) + square(4), pass it to another function, or test it in a comparison. Think of return as a vending machine: you press a button (the call), and an item drops into the tray (the value) for you to pick up.

Multiple Return Values

Plenty of real tasks produce more than one answer at once. The lowest, highest, and average of a list, for example. It works like ordering a thali at a restaurant: you place one order and several dishes arrive together on a single plate. Python lets a function return several values in one go, and you can unpack them into separate names on the way out.

📄 multi_return.py: returning several values as a tuple

def analyze(scores):
    """Return min, max, and average of a list of scores."""
    return min(scores), max(scores), sum(scores) / len(scores)

low, high, avg = analyze([88, 95, 72, 91, 84])
print(f"Low: {low}, High: {high}, Avg: {avg:.1f}")

# Early return pattern
def find_first_negative(numbers):
    for n in numbers:
        if n < 0:
            return n
    return None  # Not found

result = find_first_negative([3, 7, -2, 5])
print(f"First negative: {result}")

▶ Output

Low: 72, High: 95, Avg: 86.0
First negative: -2

What happened here: Writing return min(scores), max(scores), ... actually returns one tuple holding all three values. The line low, high, avg = analyze(...) then unpacks that tuple into three separate names in the order they came out. The second function shows the early return pattern: the moment it finds a negative number it returns and stops, and if the loop finishes with nothing found it returns None. Returning early like this keeps functions short and saves Python from checking values it no longer cares about.

Docstrings: Documenting Functions

A docstring is a short note you leave for the next person who reads your function, and that person is usually you, three months from now, with no memory of why you wrote it. Python treats it specially: a string sitting as the first line of the body becomes the function’s official documentation.

📄 docstrings.py: the Google docstring style

# Google style (most popular in web/general Python)
def calculate_bmi(weight_kg, height_m):
    """Calculate Body Mass Index.

    Args:
        weight_kg: Weight in kilograms.
        height_m: Height in meters.

    Returns:
        BMI as a float, rounded to 1 decimal.
    """
    return round(weight_kg / (height_m ** 2), 1)

print(calculate_bmi(70, 1.75))
print(calculate_bmi.__doc__)  # Access the docstring

▶ Output

22.9
Calculate Body Mass Index.

Args:
    weight_kg: Weight in kilograms.
    height_m: Height in meters.

Returns:
    BMI as a float, rounded to 1 decimal.

What happened here: The triple-quoted string on the first line of the body is the docstring. Python stashes it on the function as __doc__, and tools like help() and your editor’s tooltips read it from there. Write one for any function whose job is not obvious from its name and parameters alone.

One thing worth pointing out, because it changed recently: notice that Args: and Returns: print flush against the left edge, even though they sit indented inside the source code. Starting in Python 3.13, the interpreter strips the common leading whitespace from docstrings automatically. On Python 3.12 and earlier, the same code printed those lines with their original indentation. The text content is identical, only the leading spaces differ, so it almost never matters in practice. It is just good to know if your output looks slightly different from an older tutorial.

Imports: Using Code from Other Modules

You will not write every function yourself. Python ships with a huge standard library, and the wider world has thousands more packages. An import is how you borrow that ready-made code, the same way you grab a tool from a shared toolbox instead of forging your own hammer.

📄 imports.py: three ways to import

# Import the entire module
import math
print(f"Pi: {math.pi}")
print(f"sqrt(144): {math.sqrt(144)}")

# Import specific names
from random import randint, choice
print(f"Random 1-100: {randint(1, 100)}")
print(f"Random pick: {choice(['Rahul', 'Viraj', 'Anvay'])}")

# Import with alias
import datetime as dt
now = dt.datetime.now()
print(f"Now: {now.strftime('%Y-%m-%d %H:%M')}")

▶ Output (values vary)

Pi: 3.141592653589793
sqrt(144): 12.0
Random 1-100: 23
Random pick: Rahul
Now: 2026-06-21 14:18

What happened here: Each style pulls in code a different way. import math brings in the whole module, and you reach into it with math.pi, which keeps the source obvious. from random import randint, choice pulls just the two names you want so you can call them directly. import datetime as dt gives a long module a short nickname. The random number and timestamp will be different every time you run this, which is exactly why the output is labelled “values vary.” We cover modules in depth in the modules tutorial.

Comments vs Docstrings

People mix these two up constantly, so here is the clean split. Think of a notebook: a comment is a sticky note tucked inside a page for whoever flips it open, while a docstring is the label printed on the cover that anyone can read without opening it. A comment is a private note inside the code for whoever is reading the source. A docstring is public documentation that tools can pull out and show without anyone opening the file.

📄 comments.py: when to use a comment and when to use a docstring

# Single-line comment: explains WHY, not WHAT
tax_rate = 0.18  # GST (India's sales tax) rate as of 2026

# Multi-line comments: use several # lines
# This calculation uses the compound interest formula
# because simple interest underestimates long-term growth.

def compound_interest(principal, rate, years):
    """Calculate compound interest.

    Args:
        principal: Initial investment amount.
        rate: Annual interest rate (decimal, e.g., 0.08 for 8%).
        years: Number of years.

    Returns:
        Final amount after compound interest.
    """
    # +1 because the formula needs (1 + rate), not just rate
    return principal * (1 + rate) ** years

print(f"Result: {compound_interest(10000, 0.08, 5):.2f}")

▶ Output

Result: 14693.28

What happened here: Comments (#) explain why the code does something. Docstrings ("""...""") explain what a function does and how to call it. The skill is knowing what to leave out. Do not comment the obvious, like # increment counter sitting above counter += 1, because the code already says that. Do comment the surprising, like # +1 because the formula needs (1 + rate), where the reason is not visible in the code itself.

The Catch: None as the Default Return

This is the one mix-up that catches almost every beginner, so it gets its own section. A function that prints something feels like it gave you a value back. It did not. It is the difference between a shopkeeper announcing your total out loud and actually handing you the bill: only one of those leaves something in your hand. Forgetting that leads to a very confusing None showing up where you expected real data. In the example below, we greet a user named Prathamesh and then try to store what the function gave back.

📄 none_return.py: a function with no return hands back None

def greet(name):
    print(f"Hello, {name}!")
    # No return statement

result = greet("Prathamesh")
print(f"Result: {result}")
print(f"Type: {type(result)}")

▶ Output

Hello, Prathamesh!
Result: None
Type: <class 'NoneType'>

What happened here: greet printed its line and then ended without a return, so Python quietly handed back None. That is why result is None and its type is NoneType. The classic real-world version of this trap is sorted_list = my_list.sort(). The list does get sorted, but sort() changes the list in place and returns nothing, so sorted_list ends up as None. The habit to build: before you store a function’s result, check whether it actually returns a value or just changes something in place.

When You Will Actually Use Functions

Python functions are not an academic exercise. Here are three everyday situations where reaching for one is the obvious move.

  • You catch yourself copy-pasting. The moment you paste the same five lines a second time, stop and wrap them in a function. Say you validate a user’s email in three different places. Write is_valid_email(address) once, and every fix lands in one spot instead of three.
  • You are reading rows from a CSV (Comma-Separated Values) file. A function like clean_row(row) that strips whitespace and fixes the date format lets you run the same cleanup over every row in a loop, without restating the logic for each one.
  • A calculation is buried in a wall of code. Pulling a tax or discount formula out into calculate_total(items) gives it a name. Now the main code reads like plain English, and you can test that one piece on its own.

Common Mistakes

Mistake 1: Forgetting parentheses when calling

🚫 References the function, doesn’t call it

result = square    # This is the function object, not 49
print(result)      # <function square at 0x...>

✅ Add parentheses to call

result = square(7)  # Calls the function, returns 49

Why: The name square on its own is just a reference to the function, the way “Mom” in your phone is the contact, not the call. The () is what places the call. Without it you get the function object printed back at you, not the result you wanted.

Mistake 2: print() vs return

🚫 print() inside a function is not the same as return

def add_bad(a, b):
    print(a + b)     # Displays to screen, returns None

total = add_bad(3, 4)   # Prints 7 but total is None

✅ return gives the caller the value

def add_good(a, b):
    return a + b      # Returns 7 to the caller

total = add_good(3, 4)  # total is 7

Why: print() throws text on the screen and keeps nothing. add_bad shows 7 but hands back None, so total is useless for any further math. return passes the value back into your code, so add_good gives total the real number 7. Rule of thumb: print when a human needs to read it, return when your code needs to use it.

Best Practices

  • DO name functions with verbs: calculate_total, get_user, is_valid
  • DO keep functions short. If one runs past 20 lines, consider splitting it
  • DO write docstrings for non-trivial functions
  • DO use return to give back results, not print()
  • DON’T put mutable default arguments (like [] or {}) in function signatures
  • DON’T use global variables when you could pass them as parameters

Practice Exercises

  1. Exercise 1: Write a greeting function. Call it 3 times with different names.
  2. Exercise 2: Write power(base, exp=2) with a default parameter.
  3. Exercise 3: Write analyze_prices(prices) that returns the lowest, highest, and average price of a list, then unpack all three into separate variables and print them.

Conclusion

Python functions are how you turn code into reusable, testable pieces. def defines them, parameters let them take input, and return lets them hand back a result. Docstrings record what they do, and imports let you borrow functions other people already wrote. If you remember one thing, make it the split between print() (shows a value) and return (gives a value back). Get that right early and most function bugs simply never happen.

Next up: Function Arguments, covering default values, keyword arguments, *args, and **kwargs. And if you want to browse every post in order, from basics to AI/ML, visit the Python + AI/ML tutorial series home.

Frequently Asked Questions

What is a function in Python?

A named block of reusable code defined with def. Python functions accept parameters, execute a body, and optionally return a value. Example: def add(a, b): return a + b.

What is the difference between parameters and arguments?

Parameters are variables in the function definition: def greet(name). Arguments are the actual values passed when calling: greet('Rahul'). Parameters are placeholders; arguments are the real values.

What does return do in Python?

return ends the function and sends a value back to the caller. Without return, the function returns None. You can return any type: integers, strings, lists, tuples, or even other functions.

What is a docstring in Python?

A string literal as the first statement in a function, class, or module. It documents what the code does. Accessible via function.__doc__ or help(function). Triple quotes allow multi-line docstrings.

What is the difference between print() and return?

print() displays text on the screen, which is a side effect. return sends a value back to the calling code for further use. A function that prints its result can’t have that result used in calculations. Use return for producing values, print() for displaying them.

Interview Questions on Python Functions

Scenario questions, not trivia: this is the form this topic takes in a real interview.

Q: You wrote sorted_names = names.sort() and later your code crashes with “‘NoneType’ object is not iterable”. What went wrong?

list.sort() sorts the list in place and returns None, so sorted_names never held a list at all. The crash happens later, wherever you loop over it. The fix is either names.sort() followed by using names directly, or sorted_names = sorted(names), which returns a new sorted list. Before storing any function’s result, check whether it returns a value or modifies something in place.

Q: What is the difference between positional and keyword arguments, and in what order must they appear in a call?

Positional arguments are matched to parameters by their order, while keyword arguments are matched by name, like create_profile("Vinay", lang="JavaScript"). In a call, all positional arguments must come before any keyword arguments, or Python raises a SyntaxError. Keyword arguments are useful for skipping defaults you do not want to override and for making calls self-documenting.

Q: Your teammate defines def add_item(item, items=[]) and reports that the list keeps growing across completely unrelated calls. Why does this happen, and what is the fix?

Default argument values are evaluated once, when the def line runs, not on every call. That means every call that relies on the default shares the same single list object, so items from earlier calls stick around. The standard fix is a sentinel: use items=None in the signature and inside the function write if items is None: items = [] so each call gets a fresh list.

Q: Why does def greet(city="Mumbai", name): raise a SyntaxError?

In a function definition, parameters with default values must come after parameters without defaults. If a default came first, Python could not tell whether a lone positional argument was meant for city or name. Reorder it to def greet(name, city="Mumbai"): and the ambiguity disappears.

Q: How does Python handle a function that calls another function internally?

Python uses a call stack. Each call pushes a new frame holding that function’s local variables, and the newest frame always runs first. When the inner function returns, its frame is destroyed and the value is handed back to the frame that called it. This is why local variables in different functions never collide, and it is the structure a traceback prints when something breaks.

Q: How can a function return multiple values, and what is actually being returned under the hood?

You list the values after return separated by commas, like return low, high, avg. Under the hood Python packs them into a single tuple, and the caller usually unpacks it with low, high, avg = analyze(scores). The number of names on the left must match the tuple length, or Python raises a ValueError.

Q: When would you choose import math over from math import sqrt?

Use import math when you want the module name visible at every call site, like math.sqrt(144), which makes the origin of each function obvious in a large file. Use from math import sqrt when you call one or two names often and want shorter code. The from style can shadow or be shadowed by local names, so the whole-module import is the safer default in bigger projects.

Reference: the complete, always-current details live in the official Python documentation.

Previous: Python: Choosing the Right Data Structure

Next: Python Function Arguments: Default, Keyword, *args, **kwargs

Series Home: Python + AI/ML Tutorial Series

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *