Python Functions
def. Parameters vs arguments, default values, *args and **kwargs, return values, multiple returns. Variable scope — the LEGB rule. Lambda functions. Docstrings. And the most important principle in programming: Don't Repeat Yourself (DRY).
Why Functions?
A function is a named, reusable block of code that performs a specific task. Without functions, every time you need to calculate a grade or validate a password, you'd copy-paste the same code — creating programs that are hard to read, maintain, and fix.
Functions give you three superpowers: reusability (write once, call anywhere), abstraction (hide complexity behind a name), and testability (test one piece independently). Every professional codebase is built from functions.
# Without functions — repeated code (bad) tax1 = 1500 * 0.17 tax2 = 2800 * 0.17 tax3 = 950 * 0.17 # With a function — write once, use anywhere (DRY) def calculate_tax(amount): """Calculate 17% sales tax on an amount.""" return amount * 0.17 tax1 = calculate_tax(1500) # 255.0 tax2 = calculate_tax(2800) # 476.0 tax3 = calculate_tax(950) # 161.5 # Calling the function print(f"Tax: PKR {tax1:.2f}") # Functions without a return statement return None def say_hello(): print("Hello, BitWithBite!") # does work, returns None result = say_hello() print(result) # None
Parameters & Arguments
Parameters are the variable names in the function definition. Arguments are the actual values you pass when calling the function. Python has four parameter types — each solving a different problem.
# ── Positional and default parameters ──────── def describe_student(name, age, city="Lahore", grade="A"): return f"{name}, {age}, from {city}, Grade {grade}" print(describe_student("Ali", 20)) # uses defaults print(describe_student("Sara", 22, "Karachi")) # overrides city print(describe_student("Zara", 21, grade="B")) # keyword arg # ── *args — collect any number of positional args ── def total(*numbers): """Sum any number of arguments.""" return sum(numbers) # numbers is a tuple print(total(1, 2, 3)) # 6 print(total(10, 20, 30, 40)) # 100 print(total(5)) # 5 # ── **kwargs — collect keyword arguments as a dict ── def build_profile(**info): """Print a profile from any keyword arguments.""" for key, val in info.items(): print(f" {key.capitalize()}: {val}") build_profile(name="Fatima", age=20, city="Islamabad", gpa=3.9) # ── Combining all types (correct order!) ───── def mixed(required, optional="default", *args, **kwargs): print(required, optional, args, kwargs)
def append_to(item, lst=[]) is a classic Python bug. The default list is created once at function definition and shared across all calls. Use None as the default and create the list inside: if lst is None: lst = []. This trips up even experienced developers.Return Values
The return statement sends a value back to the caller and immediately exits the function. A function can return any Python object — a number, string, list, dict, tuple, or even another function.
# Single return value def square(n): return n ** 2 print(square(7)) # 49 # Early return — guard clause pattern def divide(a, b): if b == 0: return None # exit early on bad input return a / b print(divide(10, 2)) # 5.0 print(divide(10, 0)) # None # Multiple return values (returns a tuple) def stats(numbers): """Return min, max, average of a list.""" return min(numbers), max(numbers), sum(numbers)/len(numbers) lo, hi, avg = stats([8, 3, 15, 1, 9]) print(f"Min: {lo}, Max: {hi}, Avg: {avg:.1f}") # 1 15 7.2 # Return a dict — named results def analyse_text(text): words = text.split() return { "chars" : len(text), "words" : len(words), "unique" : len(set(words)), } r = analyse_text("to be or not to be that is the question") print(r) # {'chars': 40, 'words': 10, 'unique': 8}
result["avg"]. This is how professional APIs work and makes code self-documenting.Variable Scope — The LEGB Rule
When Python encounters a variable name, it searches for it in a specific order of scopes. This order is called the LEGB rule — Local → Enclosing → Global → Built-in.
| Letter | Scope | Where | Example |
|---|---|---|---|
| L | Local | Inside the current function | Variables created inside def |
| E | Enclosing | Inside any enclosing function (closures) | Outer function's variables |
| G | Global | Module level (top of the file) | Variables defined outside functions |
| B | Built-in | Python's built-in namespace | print, len, range, int… |
# Global scope APP_NAME = "BitWithBite" # G — global def show_app(): version = "2.0" # L — local to show_app print(APP_NAME, version) # can read global APP_NAME show_app() # print(version) ← NameError: version not defined here # Global variables are READ-ONLY inside functions by default counter = 0 def increment(): global counter # declare intent to MODIFY global counter += 1 increment() increment() print(counter) # 2 # ── Best practice: avoid global — use return instead ── def increment_v2(count): return count + 1 # pure function — no side effects counter = increment_v2(counter) # caller controls the global # Enclosing scope (closure preview) def outer(): msg = "hello from outer" def inner(): print(msg) # E — enclosing scope inner()
global — it's a code smellglobal makes functions depend on external state, which makes them hard to test and debug. Professional Python code passes values as arguments and returns new values instead of modifying globals. A function that only uses its parameters and return values is called a pure function — aim for this.Lambda Functions & Higher-Order Functions
A lambda is a small anonymous function written in one line. It's syntactic sugar for a simple function — useful when you need a short function as an argument to another function, like sorted() or map().
Higher-order functions are functions that take other functions as arguments — map(), filter(), and sorted() are the most common. Combining these with lambda makes code elegant and concise.
# Lambda syntax: lambda params: expression square = lambda x: x ** 2 print(square(5)) # 25 add = lambda a, b: a + b print(add(3, 7)) # 10 # ── sorted() with key lambda ────────────────── students = [("Ali", 85), ("Sara", 92), ("Zara", 78)] by_score = sorted(students, key=lambda s: s[1], reverse=True) print(by_score) # [('Sara', 92), ('Ali', 85), ('Zara', 78)] names = ["banana", "Apple", "mango", "cherry"] print(sorted(names, key=lambda x: x.lower())) # case-insensitive sort # ── map() — apply a function to every item ─── prices = [100, 250, 80, 320] with_tax = list(map(lambda p: p * 1.17, prices)) print(with_tax) # [117.0, 292.5, 93.6, 374.4] # ── filter() — keep items where function is True ─ marks = [55, 42, 88, 31, 76, 90] passing = list(filter(lambda m: m >= 50, marks)) print(passing) # [55, 88, 76, 90] # Passing a def function as argument def is_even(n): return n % 2 == 0 evens = list(filter(is_even, range(10))) print(evens) # [0, 2, 4, 6, 8]
Docstrings & Best Practices
A docstring is a string literal placed as the first statement of a function. It describes what the function does, its parameters, and what it returns. Python's help() function and most IDEs display docstrings automatically.
# One-line docstring (simple functions) def celsius_to_fahrenheit(c): """Convert Celsius to Fahrenheit.""" return c * 9/5 + 32 # Multi-line docstring (complex functions) def calculate_grade(score, total=100): """ Calculate a letter grade from a score. Args: score (float): The marks obtained. total (float): Maximum possible marks. Default 100. Returns: str: Letter grade A+, A, B, C, or F. Examples: >>> calculate_grade(92) 'A+' >>> calculate_grade(45, 100) 'F' """ pct = score / total * 100 if pct >= 95: return "A+" elif pct >= 85: return "A" elif pct >= 70: return "B" elif pct >= 55: return "C" else: return "F" # Access the docstring programmatically print(calculate_grade.__doc__) help(calculate_grade) # formatted output in terminal
calculate_tax(), validate_email(), get_user(), send_message(). Noun names suggest it should be a class.return statement?*args collect inside a function?lambda x: x * 2 create?def f(a, b=10): return a + b called as f(5)?1.
get_grade(score, total=100) — returns letter grade A+/A/B/C/F with docstring2.
class_stats(*scores) — takes any number of scores, returns dict with min/max/avg/count3.
build_student(**info) — takes keyword args (name, age, gpa, etc.), returns formatted student card string4.
top_students(students, n=3, key="gpa") — takes list of dicts, returns top n sorted by key5. A lambda
is_distinction = lambda s: s >= 85 used inside a list comprehension6. A main block that creates 5 student dicts, calls all functions, and prints a full report
💡 Show hints
- *scores inside function is a tuple — use sum(scores)/len(scores) for avg
- **info is a dict — loop .items() to build the card
- top_students: use sorted(students, key=lambda s: s[key], reverse=True)[:n]
- Distinction list: [s["name"] for s in students if is_distinction(s["gpa"]*25)]
# ── Student Analytics Library ───────────────── def get_grade(score, total=100): """Return letter grade. Defaults to /100.""" p = score / total * 100 if p >= 95: return "A+" elif p >= 85: return "A" elif p >= 70: return "B" elif p >= 55: return "C" else: return "F" def class_stats(*scores): """Return stats dict for any number of scores.""" if not scores: return {} return {"count": len(scores), "min": min(scores), "max": max(scores), "avg": sum(scores)/len(scores)} def build_student(**info): """Format student info from keyword arguments.""" lines = [f" {k.capitalize():10}: {v}" for k, v in info.items()] return "┌─ Student Card ─────────┐\n" + "\n".join(lines) + "\n└────────────────────────┘" def top_students(students, n=3, key="gpa"): """Return top n students sorted by key.""" return sorted(students, key=lambda s: s[key], reverse=True)[:n] is_distinction = lambda gpa: gpa >= 3.7 # ── Main ───────────────────────────────────── students = [ {"name": "Ali", "gpa": 3.85, "score": 88}, {"name": "Sara", "gpa": 3.95, "score": 96}, {"name": "Zara", "gpa": 3.40, "score": 72}, {"name": "Omar", "gpa": 3.60, "score": 79}, {"name": "Fatima", "gpa": 3.75, "score": 91}, ] scores = [s["score"] for s in students] cs = class_stats(*scores) distinctions= [s["name"] for s in students if is_distinction(s["gpa"])] top = top_students(students, n=3) print(f"Class: {cs['count']} students | Avg: {cs['avg']:.1f} | Range: {cs['min']}–{cs['max']}") print(f"Distinctions: {', '.join(distinctions)}") print("\nTop 3 by GPA:") for s in top: print(f" {s['name']:10} GPA {s['gpa']} {get_grade(s['score'])}")