Back to blog
Backend Systemsbeginner

Python Functions, File I/O, and Error Handling

Master Python functions (args, kwargs, type hints, decorators), file I/O (reading, writing, CSV, JSON), and error handling with try/except.

Asma HafeezApril 17, 20266 min read
pythonfunctionsfile-ioerror-handling
Share:𝕏

Python Functions, File I/O, and Error Handling


Functions

Defining Functions

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

result = greet("Asma")  # "Hello, Asma!"

Default Arguments

Python
def create_user(name: str, role: str = "viewer", active: bool = True) -> dict:
    return {"name": name, "role": role, "active": active}

create_user("Asma")                    # {"name":"Asma","role":"viewer","active":True}
create_user("Ali", role="admin")       # override default
create_user("Bob", "editor", False)    # positional

*args and **kwargs

Python
# *args  variable positional arguments (tuple)
def sum_all(*numbers: int) -> int:
    return sum(numbers)

sum_all(1, 2, 3, 4, 5)  # 15

# **kwargs  variable keyword arguments (dict)
def log_event(event: str, **metadata):
    print(f"[{event}]", metadata)

log_event("user.login", user_id=42, ip="192.168.1.1")
# [user.login] {'user_id': 42, 'ip': '192.168.1.1'}

# Combining
def mixed(required, *args, key="default", **kwargs):
    print(required, args, key, kwargs)

mixed("hello", 1, 2, 3, key="custom", extra="data")
# hello (1, 2, 3) custom {'extra': 'data'}

Type Hints

Python
from typing import Optional, Union, Callable

def find_user(user_id: int) -> Optional[dict]:
    # Returns a dict or None
    ...

def process(value: Union[int, str]) -> str:
    return str(value)

def apply(fn: Callable[[int], int], n: int) -> int:
    return fn(n)

# Python 3.10+  cleaner union syntax
def find(id: int) -> dict | None: ...

Lambda Functions

Python
# Short anonymous functions
double = lambda x: x * 2
add = lambda x, y: x + y

# Most useful in sort keys and higher-order functions
products = [{"name": "B", "price": 50}, {"name": "A", "price": 30}]
products.sort(key=lambda p: p["price"])
products.sort(key=lambda p: (-p["price"], p["name"]))  # price desc, name asc

# filter and map with lambda
numbers = [1, 2, 3, 4, 5, 6]
evens = list(filter(lambda x: x % 2 == 0, numbers))
doubled = list(map(lambda x: x * 2, numbers))

Decorators

Python
import time
from functools import wraps

# A decorator is a function that wraps another function
def timer(func):
    @wraps(func)  # preserves the original function's metadata
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.3f}s")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(0.1)
    return "done"

slow_function()  # prints: slow_function took 0.101s

# Decorator with arguments
def retry(max_attempts: int = 3, exceptions=(Exception,)):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts:
                        raise
                    print(f"Attempt {attempt} failed: {e}. Retrying...")
        return wrapper
    return decorator

@retry(max_attempts=3, exceptions=(ConnectionError,))
def fetch_data(url: str) -> dict:
    ...

File I/O

Reading Files

Python
# Read entire file
with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()

# Read line by line (memory efficient for large files)
with open("data.txt", "r") as f:
    for line in f:
        print(line.strip())  # strip removes trailing newline

# Read all lines into list
with open("data.txt", "r") as f:
    lines = f.readlines()        # includes newlines
    lines = [l.strip() for l in f]  # clean version

Writing Files

Python
# Write (overwrites existing file)
with open("output.txt", "w", encoding="utf-8") as f:
    f.write("Hello, World!\n")
    f.write("Second line\n")

# Append (adds to existing file)
with open("log.txt", "a") as f:
    f.write(f"[{timestamp}] User logged in\n")

# Write multiple lines
lines = ["line 1", "line 2", "line 3"]
with open("output.txt", "w") as f:
    f.writelines(f"{line}\n" for line in lines)

CSV Files

Python
import csv

# Read CSV
with open("users.csv", "r", newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)  # uses first row as field names
    for row in reader:
        print(row["name"], row["email"])

# Write CSV
users = [
    {"name": "Asma", "email": "asma@example.com", "role": "admin"},
    {"name": "Ali", "email": "ali@example.com", "role": "user"},
]
with open("users.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "email", "role"])
    writer.writeheader()
    writer.writerows(users)

JSON Files

Python
import json

# Read JSON
with open("config.json", "r") as f:
    config = json.load(f)  # dict/list

# Write JSON
data = {"name": "Asma", "scores": [95, 87, 92]}
with open("data.json", "w") as f:
    json.dump(data, f, indent=2, ensure_ascii=False)

# JSON string <-> Python object
json_str = json.dumps(data)               # Python  JSON string
python_obj = json.loads(json_str)         # JSON string  Python

# Custom serialization
from datetime import datetime
def json_default(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    raise TypeError(f"Not serializable: {type(obj)}")

json.dumps({"created": datetime.now()}, default=json_default)

Error Handling

try / except / else / finally

Python
def divide(a: float, b: float) -> float:
    try:
        result = a / b
    except ZeroDivisionError:
        print("Cannot divide by zero")
        return 0.0
    except TypeError as e:
        print(f"Wrong types: {e}")
        raise  # re-raise the exception
    else:
        # Only runs if no exception was raised
        print(f"Result: {result}")
        return result
    finally:
        # Always runs  use for cleanup
        print("Division attempted")

Catching Multiple Exceptions

Python
try:
    value = int(input("Enter a number: "))
    result = 100 / value
except (ValueError, ZeroDivisionError) as e:
    print(f"Error: {e}")

Custom Exceptions

Python
class AppError(Exception):
    """Base exception for the application."""
    pass

class ValidationError(AppError):
    def __init__(self, field: str, message: str):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")

class NotFoundError(AppError):
    def __init__(self, resource: str, id: int):
        super().__init__(f"{resource} with id {id} not found")

# Usage
def get_user(user_id: int) -> dict:
    user = db.find(user_id)
    if user is None:
        raise NotFoundError("User", user_id)
    return user

def validate_age(age: int):
    if age < 0 or age > 150:
        raise ValidationError("age", f"must be between 0 and 150, got {age}")

# Catching specific hierarchy
try:
    validate_age(-5)
except ValidationError as e:
    print(f"Validation failed on '{e.field}': {e.message}")
except AppError as e:
    print(f"Application error: {e}")

Context Managers

The with statement ensures cleanup happens even if an exception occurs.

Python
# File (built-in context manager)
with open("file.txt") as f:
    data = f.read()
# f.close() is called automatically

# Custom context manager
from contextlib import contextmanager

@contextmanager
def timer():
    start = time.perf_counter()
    try:
        yield  # code in the `with` block runs here
    finally:
        elapsed = time.perf_counter() - start
        print(f"Elapsed: {elapsed:.3f}s")

with timer():
    time.sleep(0.5)
# Elapsed: 0.500s

Key Takeaways

  1. Use *args and **kwargs to write flexible, reusable functions
  2. Type hints make code self-documenting and enable IDE autocompletion
  3. Always use with open(...) for files — it guarantees the file is closed
  4. csv.DictReader and json.load** for structured file reading — don't parse manually
  5. Custom exceptions make error handling specific and debuggable
  6. Decorators let you add cross-cutting concerns (logging, retry, caching) without modifying the original function

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.