Latest update Android YouTube

Functions | Basics of Python Developer

Python Functions: A Deep Dive

Mastering one of Python's most fundamental building blocks

1. Function Definition

Basics

Functions in Python are defined using the def keyword followed by the function name and parentheses.

def greet(name):
    """Return a greeting message."""
    return f"Hello, {name}!"

Function naming conventions

  • Use snake_case (all lowercase with underscores)
  • Should be descriptive but concise
  • Verbs for actions (e.g., calculate_total)
  • Follow PEP 8 guidelines

The pass keyword

Use pass as a placeholder for empty functions:

def todo_function():
    pass  # Implement later

Defining vs calling

# Defining a function
def say_hello():
    print("Hello!")

# Calling the function
say_hello()  # Output: Hello!

Advanced Concepts

First-class functions

Functions are objects that can be assigned to variables, passed as arguments, and returned from other functions.

def square(x):
    return x * x

# Assign function to variable
func = square
print(func(5))  # 25

# Pass function as argument
def apply_func(f, x):
    return f(x)

print(apply_func(square, 4))  # 16

Docstrings and __doc__

Document functions with docstrings (following PEP 257):

def calculate_area(width, height):
    """Calculate the area of a rectangle.
    
    Args:
        width (float): The width of the rectangle
        height (float): The height of the rectangle
        
    Returns:
        float: The area (width * height)
    """
    return width * height

# Access documentation
print(calculate_area.__doc__)
help(calculate_area)

Function annotations

Type hints for parameters and return values (PEP 484):

def repeat(text: str, count: int) -> str:
    """Return text repeated count times."""
    return text * count

Namespace and scope

Functions have access to their local scope and enclosing scopes:

x = "global"

def outer():
    x = "outer"
    
    def inner():
        nonlocal x  # Refers to x in outer()
        x = "inner"
    inner()
    print(x)  # "inner"

outer()
print(x)  # "global"

2. Parameters and Arguments

Parameter Types

Positional arguments

Matched by position in the function call:

def power(base, exponent):
    return base ** exponent

print(power(2, 3))  # 8 (2 is base, 3 is exponent)

Keyword arguments

Matched by parameter name:

print(power(exponent=3, base=2))  # 8

Default parameters

Parameters with default values become optional:

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

greet()          # Hello, World!
greet("Alice")   # Hello, Alice!

Warning: Mutable default arguments are a common pitfall (see Advanced Concepts below).

Variable-length arguments

Collect arbitrary numbers of arguments:

def sum_all(*args):
    """Sum all positional arguments."""
    return sum(args)

print(sum_all(1, 2, 3))  # 6

def print_details(**kwargs):
    """Print all keyword arguments."""
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_details(name="Alice", age=25)

Advanced Concepts

Argument unpacking

Use * and ** to unpack sequences and dictionaries:

def point(x, y):
    print(f"Point at ({x}, {y})")

coordinates = (3, 4)
point(*coordinates)  # Point at (3, 4)

params = {'x': 1, 'y': 2}
point(**params)      # Point at (1, 2)

Mutable default arguments

Default arguments are evaluated once when the function is defined:

# Anti-pattern
def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2] - Surprise!

# Correct approach
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

Keyword-only arguments

Force arguments to be passed by keyword (PEP 3102):

def create_user(name, *, email, phone):
    """Parameters after * must be keyword arguments"""
    print(f"Creating {name} with {email} and {phone}")

create_user("Alice", email="a@example.com", phone="1234")  # OK
# create_user("Bob", "b@example.com", "5678")  # Error

Positional-only arguments

Force arguments to be positional (PEP 570, Python 3.8+):

def power(base, exponent, /):
    """Parameters before / must be positional"""
    return base ** exponent

power(2, 3)  # OK
# power(base=2, exponent=3)  # Error

3. Built-in Functions

Commonly Used Built-ins

Category Functions Description
I/O print(), input(), open() Basic input/output operations
Type Conversion int(), str(), list(), dict() Convert between types
Math abs(), round(), min(), max(), sum() Mathematical operations
Iteration len(), range(), enumerate(), zip() Working with sequences

Advanced Built-ins

Functional Programming

# map() - Apply function to each item
numbers = [1, 2, 3]
squared = list(map(lambda x: x**2, numbers))  # [1, 4, 9]

# filter() - Select items where function returns True
evens = list(filter(lambda x: x % 2 == 0, numbers))  # [2]

# reduce() - Cumulatively apply function (from functools)
from functools import reduce
product = reduce(lambda x, y: x * y, numbers)  # 6

Dynamic Code Execution

Security Warning: eval() and exec() can execute arbitrary code and should be used with extreme caution.

# eval() - Evaluate expression
result = eval("2 + 3 * 4")  # 14

# exec() - Execute statements
exec("""
def hello():
    print("Hello from exec!")
""")
hello()  # Hello from exec!

Type Checking

print(isinstance(42, int))      # True
print(issubclass(bool, int))  # True (bool is a subclass of int)

Namespace Inspection

def show_locals():
    x = 10
    print(locals())  # {'x': 10}

show_locals()
print(globals().keys())  # Shows global names

4. Lambda Functions

Basics

Lambda functions are small anonymous functions defined with the lambda keyword.

# Regular function
def square(x):
    return x * x

# Equivalent lambda
square = lambda x: x * x

Use cases

Best for short, one-time operations where a full function definition would be verbose:

# Sorting with a key
names = ["Alice", "Bob", "Charlie"]
names.sort(key=lambda name: len(name))  # Sort by length

Limitations

  • Single expression only (no statements)
  • No annotations or documentation string
  • Often less readable than named functions for complex logic

Advanced Usage

With built-in functions

# map() example
numbers = [1, 2, 3]
squared = list(map(lambda x: x**2, numbers))  # [1, 4, 9]

# filter() example
evens = list(filter(lambda x: x % 2 == 0, numbers))  # [2]

# sorted() with custom key
people = [{"name": "Alice", "age": 25}, {"name": "Bob", "age": 30}]
sorted_people = sorted(people, key=lambda p: p["age"])

Closures and late binding

A common gotcha with lambdas in loops:

# Problematic - all lambdas use last i value
functions = []
for i in range(3):
    functions.append(lambda: i)

print([f() for f in functions])  # [2, 2, 2]

# Solution - capture current i value
functions = []
for i in range(3):
    functions.append(lambda i=i: i)  # Default arg captures current i

print([f() for f in functions])  # [0, 1, 2]

When not to use lambdas

Avoid lambdas when:

  • The logic is complex enough to need a docstring
  • The function is used in multiple places
  • You need type hints or other function metadata
  • The expression becomes hard to read

5. Decorators

Basic Decorators

Decorators modify or enhance functions without changing their source code.

What is a decorator?

A function that takes another function and extends its behavior.

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()
# Output:
# Something is happening before the function is called.
# Hello!
# Something is happening after the function is called.

Creating a simple decorator

def timer(func):
    """Measure and print execution time of a function."""
    import time
    
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} executed in {end-start:.4f}s")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)

slow_function()  # slow_function executed in 1.0001s

Common use cases

  • Logging
  • Timing
  • Authentication/authorization
  • Caching/memoization
  • Validation

Advanced Decorator Concepts

Decorators with arguments

Requires an extra level of nesting:

def repeat(num_times):
    """Decorator that repeats function call num_times."""
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(num_times=3)
def greet(name):
    print(f"Hello {name}")

greet("Alice")
# Output:
# Hello Alice
# Hello Alice
# Hello Alice

Class-based decorators

Implement __call__ to make instances callable:

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.num_calls = 0
    
    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"Call {self.num_calls} of {self.func.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def say_hello():
    print("Hello")

say_hello()  # Call 1 of say_hello
say_hello()  # Call 2 of say_hello

Preserving metadata with functools.wraps

Decorators obscure the original function's metadata. functools.wraps fixes this:

from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """Wrapper docstring"""
        return func(*args, **kwargs)
    return wrapper

@decorator
def example():
    """Example docstring"""
    pass

print(example.__name__)  # "example" (without wraps: "wrapper")
print(example.__doc__)   # "Example docstring"

Stacking decorators

Decorators are applied from bottom to top:

@decorator1
@decorator2
@decorator3
def my_function():
    pass

# Equivalent to:
my_function = decorator1(decorator2(decorator3(my_function)))

Built-in decorators

Decorator Description
@staticmethod Method that doesn't receive self/cls
@classmethod Method that receives class as first arg
@property Define getter for an attribute
@functools.lru_cache Memoization decorator
@dataclasses.dataclass Auto-generate special methods for classes

6. Function Execution & Memory

Stack Frames

Python maintains a call stack with frame objects for each function call:

import inspect

def show_stack():
    for frame in inspect.stack():
        print(frame.function, frame.lineno)

def outer():
    show_stack()

outer()

Recursion

Python has a recursion limit (usually 1000) to prevent stack overflows:

import sys

def factorial(n):
    return 1 if n <= 1 else n * factorial(n - 1)

print(factorial(5))  # 120

# Change recursion limit (use with caution!)
sys.setrecursionlimit(2000)

Generator Functions

Functions that use yield return a generator iterator:

def count_up_to(max):
    count = 1
    while count <= max:
        yield count
        count += 1

counter = count_up_to(5)
print(next(counter))  # 1
print(next(counter))  # 2

# Iterate through remaining
for num in counter:
    print(num)  # 3, 4, 5

7. Error Handling in Functions

Raising Exceptions

def validate_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")
    return age

try:
    validate_age(-5)
except ValueError as e:
    print(f"Error: {e}")

Custom Exceptions

class InvalidEmailError(Exception):
    """Raised when an invalid email is provided"""
    pass

def validate_email(email):
    if "@" not in email:
        raise InvalidEmailError(f"Invalid email: {email}")

try:
    validate_email("user.example.com")
except InvalidEmailError as e:
    print(e)

8. Functional Programming in Python

Pure Functions

Functions with no side effects that always return the same output for the same input:

# Pure function
def square(x):
    return x * x

# Not pure (has side effect)
def square_impure(x):
    print("Squaring", x)
    return x * x

Higher-Order Functions

Functions that take or return other functions:

def make_adder(n):
    """Return a function that adds n to its argument."""
    def adder(x):
        return x + n
    return adder

add5 = make_adder(5)
print(add5(3))  # 8

Partial Function Application

Fix some arguments of a function to create a new function:

from functools import partial

def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)

print(square(5))  # 25
print(cube(3))    # 27

Post a Comment

Feel free to ask your query...
Cookie Consent
We serve cookies on this site to analyze traffic, remember your preferences, and optimize your experience.
Oops!
It seems there is something wrong with your internet connection. Please connect to the internet and start browsing again.
AdBlock Detected!
We have detected that you are using adblocking plugin in your browser.
The revenue we earn by the advertisements is used to manage this website, we request you to whitelist our website in your adblocking plugin.
Site is Blocked
Sorry! This site is not available in your country.