DEV Community

Ramesh S
Ramesh S

Posted on

Python for Beginners — Part 6: Functions

Part 6 of a beginner-friendly series on learning Python from scratch.

In Part 5, we learned to organize data with lists, dictionaries, and other collections. Now it's time to organize our code itself.

A function is a reusable block of code that performs a specific task. Instead of writing the same code multiple times, you write it once in a function, then call that function whenever you need it. This is the foundation of writing clean, maintainable programs.

Defining and Calling Functions

The basics

def greet():
    print("Hello, World!")

greet()  # Call the function
Enter fullscreen mode Exit fullscreen mode

Use def to define a function. The function name is followed by parentheses and a colon. The indented block below is the function's body.

When you call the function (by typing its name with parentheses), Python runs the code inside it.

Functions with parameters

Most functions need information to work with. That's what parameters are for:

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

greet("Ramesh")   # Hello, Ramesh!
greet("Priya")    # Hello, Priya!
Enter fullscreen mode Exit fullscreen mode

name is a parameter (placeholder). When you call greet("Ramesh"), name becomes "Ramesh" inside the function.

Multiple parameters:

def add(x, y):
    print(x + y)

add(5, 3)   # 8
add(10, 20) # 30
Enter fullscreen mode Exit fullscreen mode

Return values

A function can calculate something and give the result back to you with return:

def add(x, y):
    return x + y

result = add(5, 3)
print(result)       # 8
Enter fullscreen mode Exit fullscreen mode

The return statement stops the function and sends a value back. The caller can then use that value.

def greet(name):
    message = f"Hello, {name}!"
    return message

greeting = greet("Ramesh")
print(greeting)  # Hello, Ramesh!
Enter fullscreen mode Exit fullscreen mode

A function can return multiple values as a tuple:

def get_user_info():
    return "Ramesh", 25, "Chennai"

name, age, city = get_user_info()
print(name, age, city)  # Ramesh 25 Chennai
Enter fullscreen mode Exit fullscreen mode

Arguments: Positional vs Keyword

There are two ways to pass values to a function:

Positional arguments

Arguments are matched by position:

def describe(name, age):
    print(f"{name} is {age} years old")

describe("Ramesh", 25)  # Ramesh is 25 years old
describe(25, "Ramesh")  # 25 is Ramesh years old (wrong order, confusing)
Enter fullscreen mode Exit fullscreen mode

Order matters.

Keyword arguments

You can use parameter names to be explicit:

def describe(name, age):
    print(f"{name} is {age} years old")

describe(age=25, name="Ramesh")  # Ramesh is 25 years old
describe(name="Ramesh", age=25)  # Ramesh is 25 years old (order doesn't matter)
Enter fullscreen mode Exit fullscreen mode

Keyword arguments make code clearer and are less error-prone. Mix them freely:

describe("Ramesh", age=25)  # First positional, then keyword
Enter fullscreen mode Exit fullscreen mode

Default Arguments

You can give parameters default values — they're optional:

def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(greet("Ramesh"))              # Hello, Ramesh!
print(greet("Ramesh", "Hi"))        # Hi, Ramesh!
print(greet("Ramesh", greeting="Hey"))  # Hey, Ramesh!
Enter fullscreen mode Exit fullscreen mode

If the caller doesn't provide greeting, it defaults to "Hello".

Important rule: Parameters with defaults must come after parameters without defaults:

# Correct
def add(x, y, verbose=False):
    result = x + y
    if verbose:
        print(f"{x} + {y} = {result}")
    return result

# Wrong — SyntaxError
def add(x=0, y):
    return x + y
Enter fullscreen mode Exit fullscreen mode

*args — Variable Number of Arguments

Sometimes you want a function to accept any number of arguments. Use *args:

def add_many(*numbers):
    total = 0
    for num in numbers:
        total += num
    return total

print(add_many(1, 2, 3))           # 6
print(add_many(1, 2, 3, 4, 5))     # 15
print(add_many())                  # 0 (no arguments)
Enter fullscreen mode Exit fullscreen mode

*args (the name args is conventional) collects all extra arguments into a tuple. The * unpacks them.

def print_all(*items):
    for item in items:
        print(item)

print_all("apple", "banana", "cherry", 42)
# Prints each on its own line
Enter fullscreen mode Exit fullscreen mode

**kwargs — Keyword Arguments as Dictionary

Similar to *args, use **kwargs to accept any number of keyword arguments:

def make_profile(**info):
    for key, value in info.items():
        print(f"{key}: {value}")

make_profile(name="Ramesh", age=25, city="Chennai")
# Output:
# name: Ramesh
# age: 25
# city: Chennai
Enter fullscreen mode Exit fullscreen mode

**kwargs (convention) collects keyword arguments into a dictionary.

Combining everything

You can use *args and **kwargs together:

def my_function(required, *args, default_arg="default", **kwargs):
    print(f"Required: {required}")
    print(f"Args: {args}")
    print(f"Default: {default_arg}")
    print(f"Kwargs: {kwargs}")

my_function(1, 2, 3, 4, default_arg="custom", extra1="value1", extra2="value2")

# Output:
# Required: 1
# Args: (2, 3, 4)
# Default: custom
# Kwargs: {'extra1': 'value1', 'extra2': 'value2'}
Enter fullscreen mode Exit fullscreen mode

Order: positional, *args, keyword defaults, **kwargs.

Scope and Variable Lifetime

Variables have scope — the region of code where they exist and are accessible.

Local scope

Variables created inside a function only exist inside that function:

def greet():
    message = "Hello"  # Local variable
    print(message)

greet()           # Hello
print(message)    # NameError — message doesn't exist here
Enter fullscreen mode Exit fullscreen mode

Global scope

Variables created outside functions are global:

message = "Hello"  # Global variable

def greet():
    print(message)  # Can access global variables

greet()    # Hello
print(message)  # Hello
Enter fullscreen mode Exit fullscreen mode

Modifying global variables

Be careful — inside a function, assignment creates a local variable:

count = 0

def increment():
    count = count + 1  # UnboundLocalError — trying to use local before assignment
    return count

increment()
Enter fullscreen mode Exit fullscreen mode

To modify a global variable inside a function, use global:

count = 0

def increment():
    global count
    count = count + 1
    return count

increment()
print(count)  # 1
Enter fullscreen mode Exit fullscreen mode

Tip: Generally, avoid modifying global variables. It makes code hard to understand. Instead, pass values in and return values out.

Lambda Functions — Quick Throwaway Functions

A lambda is a tiny anonymous function, useful for one-liners:

square = lambda x: x ** 2
print(square(5))  # 25
Enter fullscreen mode Exit fullscreen mode

Syntax: lambda parameters: expression

Lambda functions are usually passed to other functions. For example, sorted() can take a function that determines the sort order:

people = [
    {"name": "Ramesh", "age": 25},
    {"name": "Priya", "age": 20},
    {"name": "Arun", "age": 30}
]

# Sort by age
sorted_by_age = sorted(people, key=lambda person: person["age"])
print(sorted_by_age)
# [{'name': 'Priya', 'age': 20}, {'name': 'Ramesh', 'age': 25}, {'name': 'Arun', 'age': 30}]
Enter fullscreen mode Exit fullscreen mode

Or with map() to transform a list:

numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x ** 2, numbers))
print(squared)  # [1, 4, 9, 16, 25]
Enter fullscreen mode Exit fullscreen mode

Lambda is elegant for simple operations. For anything complex, write a proper function.

Recursion — Functions Calling Themselves

A recursive function calls itself. It's powerful for solving problems that repeat the same pattern at smaller scales.

Every recursive function needs a base case (when to stop) and a recursive case (how to shrink the problem):

def countdown(n):
    # Base case
    if n == 0:
        print("Blastoff!")
        return

    # Recursive case
    print(n)
    countdown(n - 1)

countdown(5)
# Output:
# 5
# 4
# 3
# 2
# 1
# Blastoff!
Enter fullscreen mode Exit fullscreen mode

Without a base case, the function recurses infinitely and crashes:

def bad_countdown(n):
    print(n)
    bad_countdown(n - 1)  # Never stops!

bad_countdown(5)  # RecursionError: maximum recursion depth exceeded
Enter fullscreen mode Exit fullscreen mode

Classic example: Factorial

def factorial(n):
    # Base case
    if n == 0 or n == 1:
        return 1

    # Recursive case
    return n * factorial(n - 1)

print(factorial(5))  # 120 (5 * 4 * 3 * 2 * 1)
Enter fullscreen mode Exit fullscreen mode

Recursion is elegant but can be slow and memory-intensive. Use it when the problem naturally breaks into recursive patterns (like tree structures), not for simple loops.

Generators and Iterators

A generator is a function that yields values one at a time, instead of returning them all at once. Use yield:

def count_up_to(n):
    i = 1
    while i <= n:
        yield i
        i += 1

for num in count_up_to(5):
    print(num)

# Output:
# 1
# 2
# 3
# 4
# 5
Enter fullscreen mode Exit fullscreen mode

The function pauses at yield and resumes where it left off. This is memory-efficient for large datasets:

# Without generator — creates entire list in memory
def range_list(n):
    result = []
    for i in range(n):
        result.append(i)
    return result

# With generator — creates values on-demand
def range_gen(n):
    for i in range(n):
        yield i

# Both work with for loops, but generator is more efficient
for num in range_list(1000000):  # Creates 1 million item list
    print(num)

for num in range_gen(1000000):   # Creates values as needed
    print(num)
Enter fullscreen mode Exit fullscreen mode

Generators are a more advanced topic — understand them conceptually now; you'll use them more as you progress.

Practical Examples

Example 1: Simple calculator

def add(x, y):
    return x + y

def subtract(x, y):
    return x - y

def multiply(x, y):
    return x * y

def divide(x, y):
    if y == 0:
        return "Cannot divide by zero"
    return x / y

print(add(10, 5))       # 15
print(divide(10, 0))    # Cannot divide by zero
Enter fullscreen mode Exit fullscreen mode

Example 2: Processing collections

def find_max(numbers):
    if len(numbers) == 0:
        return None

    max_num = numbers[0]
    for num in numbers:
        if num > max_num:
            max_num = num
    return max_num

print(find_max([3, 7, 2, 9, 1]))  # 9
Enter fullscreen mode Exit fullscreen mode

Example 3: Flexible greeting

def create_greeting(*names, greeting="Hello", punctuation="!"):
    message = f"{greeting}, {', '.join(names)}{punctuation}"
    return message

print(create_greeting("Ramesh", "Priya"))
# Hello, Ramesh, Priya!

print(create_greeting("Alice", "Bob", greeting="Hi", punctuation="?"))
# Hi, Alice, Bob?
Enter fullscreen mode Exit fullscreen mode

Why This Matters

Functions are how you stop repeating yourself. Instead of copying the same code 10 times, you write it once, call it 10 times. This saves time, prevents bugs, and makes changes easier.

The most common beginner mistakes:

  • Forgetting parentheses when calling a function
  • Using return to print (they're different — one sends a value back, one displays it)
  • Trying to use variables defined inside a function outside of it
  • Infinite recursion (forgetting the base case)
  • Confusing *args with * unpacking operator

What's Next

In Part 7, we explored with modules, errors, and files — using other people's code, handling problems gracefully, and reading/writing data to disk. Functions you write will often read or write files.


This is Part 6 of an 8-part beginner Python series. Catch up on Part 1: Getting Started & Syntax, Part 2: Variables, Data Types & Numbers, Part 3: Strings & Booleans, Part 4: Operators & Control Flow, and Part 5: Collections.

Top comments (1)

Collapse
 
topstar_ai profile image
Luis

This is a really interesting claim, but it’s worth reading it with a bit of skepticism and context.

A 3B parameter model “beating Opus 4.5 on reasoning” is almost certainly not a general capability comparison — it usually means a very narrow benchmark, curated tasks, or specific evaluation setup where distillation, prompting, or dataset bias can heavily influence results.

That said, the underlying idea is still important: smaller models can absolutely punch above their weight when they’re:

fine-tuned on tightly scoped reasoning tasks
paired with strong inference-time techniques (self-consistency, tool use, chain-of-thought scaffolding)
or optimized for specific domains instead of general intelligence

The real takeaway isn’t “small model beats frontier model,” but rather that reasoning performance is increasingly shaped by system design, not just raw parameter count.

If anything, this trend reinforces where the field is heading: better orchestration and training strategy often matter more than scaling alone 🤝