DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
Getting Started
01
What is Python and Why Learn It in 2026
02
Installing Python on Windows, Mac, and Linux
03
Setting up VS Code / PyCharm for Python
04
Understanding the Python Interpreter and REPL
05
Writing and Running Your First Python Program
06
Python Syntax and Indentation Rules
Variables and Data Types
01
Variables and Naming Conventions in Python
02
Numbers in Python (int, float, complex)
03
Strings and String Methods in Python
04
Booleans in Python
05
Type Conversion / Casting in Python
06
Understanding the None Type in Python
Operators
01
Arithmetic Operators in Python
02
Comparison Operators in Python
03
Logical Operators in Python
04
Assignment Operators in Python
05
Bitwise Operators in Python
06
Identity (is) and Membership (in) Operators
07
Walrus Operator (:=) in Python
Input, Output & Strings
01
input() and print() Functions in Python
02
f-strings and String Formatting in Python
03
String Slicing and Indexing in Python
04
Common String Methods (split, join, strip, replace)
05
Escape Characters in Python Strings
Control Flow
01
if, elif, else Statements in Python
02
Nested Conditionals in Python
03
for Loops in Python
04
while Loops in Python
05
break, continue, and pass Statements
06
Loop with else Clause in Python
07
match-case (Structural Pattern Matching)
Data Structures
01
Python Lists: Creation, Indexing, Slicing, Methods
02
Python Tuples and Immutability
03
Python Sets and Set Operations
04
Python Dictionaries: Keys, Values, and Methods
05
List Comprehensions in Python
06
Dictionary & Set Comprehensions in Python
07
Working with Nested Lists and Dictionaries
Functions
01
Defining and Calling Functions in Python
02
Function Arguments: Positional, Keyword, Default
03
*args and **kwargs in Python
04
Return Statements in Python Functions
05
Lambda Functions in Python
06
Recursion in Python
07
Variable Scope: Local, Global, Nonlocal
08
Docstrings and Function Annotations
Functional Programming Basics
01
map(), filter(), and reduce() in Python
02
Closures in Python Explained
03
Python Decorators: Basic to Advanced
04
Generators and yield in Python
Object-Oriented Programming (OOP)
01
Classes and Objects in Python
02
The init Constructor in Python
03
Instance vs Class Variables in Python
04
Inheritance in Python (Single, Multiple, Multilevel)
05
Polymorphism in Python
06
Encapsulation in Python
07
Abstraction in Python with the abc Module
08
Magic/Dunder Methods in Python
09
Static and Class Methods in Python
10
The @property Decorator in Python
11
Python Dataclasses Explained
Error Handling
01
try, except, finally in Python
02
Handling Multiple Exceptions in Python
03
Raising Custom Exceptions in Python
04
Understanding Python's Exception Hierarchy
05
Assertions in Python
File Handling
01
Reading and Writing Text Files in Python
02
Working with the with Statement in Python
03
Working with CSV Files in Python
04
Working with JSON Files in Python
05
Working with Directories (os, pathlib) in Python
Modules and Packages
01
Importing Built-in Modules in Python
02
Creating Your Own Modules in Python
03
Understanding Packages and init.py
04
Python Standard Library Overview
05
Installing Packages with pip
Advanced Core Concepts
01
Iterators and Iterables in Python
02
Context Managers in Python (with, contextlib)
03
Multithreading Basics in Python
04
Multiprocessing Basics in Python
05
Asyncio Basics: async/await in Python
06
Memory Management and Garbage Collection in Python
07
Type Hints in Python (typing module)

Defining and Calling Functions in Python

Every program past a certain size needs a way to package up repeated logic into something reusable. That's what python functions are for — this article covers how to define one with the def keyword, how to call it, and how the return statement sends a result back to whoever called it.

Introduction: Why Functions Matter

A function is a reusable, named block of code that performs a specific task. Instead of writing the same logic over and over throughout your program, you write it once, give it a name, and invoke that name whenever you need it.

Core benefit

Functions solve two related problems at once: they eliminate repeated code (write the logic once, use it everywhere), and they organize a program into logical, testable pieces — instead of one long, undifferentiated block of instructions, your program becomes a collection of clearly named, individually understandable steps.

A real-world framing

Think of a function like a custom setting on an appliance — a "delicate cycle" on a washing machine, say. You configure exactly what that setting does once, give it a name, and from then on you just select it whenever you need that specific behavior, without re-specifying every detail each time.

Defining a Function

Basic syntax
def function_name():
    # indented body — the code that runs when the function is called

Four pieces make up a function definition: the def keyword, the function's name, parentheses (which may or may not contain parameters, covered next), and a colon — followed by an indented block, exactly the same indentation rules covered in the earlier syntax article.

Naming rules

Function names follow the same rules and conventions covered in the earlier variables article: they must start with a letter or underscore, contain only letters/numbers/underscores, and by PEP 8 convention, use snake_case — descriptive, lowercase, words separated by underscores.

def calculate_total():   # good — descriptive, snake_case
    pass

def calc():               # works, but too vague to be useful later
    pass
A simple example with no parameters
def greet():
    print("Hello there!")
Important: defining doesn't execute

This is genuinely worth pausing on, because it trips up nearly every beginner at least once: writing a function definition doesn't run any of the code inside it. The body only executes when the function is actually called.

def greet():
    print("Hello there!")

# Nothing has printed yet — the function was only defined, not called

If you run a script containing just that definition and nothing else, it produces no output at all. The function exists, ready to be used, but nothing inside it has happened yet.

Calling a Function and Passing Arguments

Basic call syntax

To actually run a function's code, you call it by name, followed by parentheses:

def greet():
    print("Hello there!")

greet()   # Hello there!
Parameters vs. arguments

This distinction is a genuinely common point of beginner confusion, and it's worth being precise about: parameters are the placeholder names listed in a function's definition. Arguments are the actual values you pass in when calling the function.

def greet(name):   # 'name' is a parameter
    print(f"Hello, {name}!")

greet("Alex")   # "Alex" is the argument

In casual conversation, people often use "parameter" and "argument" interchangeably, and in practice it rarely causes real confusion — but knowing the precise distinction helps when reading more formal documentation or error messages, which are usually careful about which term they use.

Argument order matters

When you call a function with multiple arguments and don't specify which parameter each one belongs to, Python matches them up by position — the first argument fills the first parameter, the second fills the second, and so on:

def describe_pet(name, animal_type):
    print(f"{name} is a {animal_type}")

describe_pet("Rex", "dog")     # Rex is a dog
describe_pet("dog", "Rex")     # dog is a Rex — technically valid, but clearly wrong

Both calls run without error, but only the first one produces the intended result — the second silently swaps the meaning of the two arguments, since Python has no way to know your actual intent from position alone. (A later article in this series covers keyword arguments, which sidestep this exact problem by letting you name which parameter each value goes to.)

Returning Values with return

What return does
The return statement sends a value back to wherever the function was called, and immediately ends the function's execution — nothing after a return statement (within that function call) ever runs.
def add(a, b):
    return a + b

result = add(3, 5)
print(result)   # 8
Functions without an explicit return

If a function never hits a return statement — or hits a bare return with nothing after it — it implicitly returns None, a behavior covered in more detail in the earlier article on the None type:

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

result = greet("Alex")
print(result)   # None — even though the function did run and print something

This is worth remembering: a function doing something (like printing) is entirely separate from a function returning something. A function can do plenty of visible work and still hand back None to its caller, if it never explicitly returns a value.

Returning multiple values at once

Python lets you return more than one value from a single function — under the hood, this actually packs the values into a tuple, which you can then unpack at the call site:

def get_min_max(numbers):
    return min(numbers), max(numbers)

lowest, highest = get_min_max([4, 7, 1, 9, 3])
print(lowest, highest)   # 1 9

return min(numbers), max(numbers) is really returning a single tuple, (1, 9) — and the multiple assignment on the calling side (covered in the earlier assignment operators article) unpacks that tuple directly into lowest and highest.

Calculating vs. printing: a critical distinction
# This function calculates and returns a value — the caller decides what to do with it
def add_tax(price, tax_rate):
    return price * (1 + tax_rate)

total = add_tax(50, 0.08)
print(total)   # 54.0

# This function only prints — it doesn't hand anything back for further use
def show_total(price, tax_rate):
    print(price * (1 + tax_rate))

result = show_total(50, 0.08)   # prints 54.0
print(result)                    # None — nothing was actually returned

The first version is generally more useful and flexible — the caller gets an actual value back and can do whatever it wants with it (store it, pass it to another function, format it for display). The second version locks the function into one specific behavior (printing), and gives the caller nothing usable in return.

Practical Example and Common Pitfalls

A more complete example

Here's a function that combines a parameter, a loop, and a return value — filtering a list down to just its even numbers:

def get_even_numbers(numbers):
    evens = []
    for number in numbers:
        if number % 2 == 0:
            evens.append(number)
    return evens

result = get_even_numbers([1, 2, 3, 4, 5, 6])
print(result)   # [2, 4, 6]

This small example brings together several concepts from earlier articles in this series — a for loop, an if condition, list building with .append() — all wrapped inside a reusable, named function with a clear input and a clear output.

Common beginner mistakes

Forgetting return and expecting output. This is probably the single most common early mistake:

def add(a, b):
    a + b   # calculated, but never returned!

result = add(3, 5)
print(result)   # None — not 8

The calculation happened, but without return, the result was simply discarded the moment the function finished — Python doesn't automatically hand back the value of the last expression the way some other languages do.

Mismatched argument counts. Calling a function with too few or too many arguments raises an error immediately:

def greet(name):
    print(f"Hello, {name}!")

greet()
# TypeError: greet() missing 1 required positional argument: 'name'

Confusing print() inside a function with actually returning a value. As covered above, a function can print something and still return None — these are two entirely separate behaviors, and mixing them up (assuming a function's printed output is also its return value) is a genuinely common source of confusing bugs.

A preview: default argument values

One more thing worth flagging before the next article in this series, which covers parameters and arguments in much more depth: parameters can have default values, letting a caller omit an argument entirely if the default is acceptable:

def greet(name="stranger"):
    print(f"Hello, {name}!")

greet()          # Hello, stranger!
greet("Alex")    # Hello, Alex!

This is genuinely useful — but it comes with a well-known gotcha worth knowing about early: using a mutable object (like a list or dictionary) as a default value can cause that same object to be unexpectedly shared and mutated across multiple separate calls to the function, in ways that surprise almost everyone the first time they hit it. The next article covers this in full, along with the standard fix — but for now, the safe rule of thumb is: stick to immutable defaults (numbers, strings, None, True/False) unless you specifically understand why a mutable one would be safe in your situation.

PREVIOUSNEXT LESSON