Python course Part 4: Functions & Scope (Writing Clean Code)

In our first three parts we wrote "procedural" scripts. The interpreter started at line 1 and ran straight to the bottom.
That's fine for short scripts, but it falls apart as a codebase grows. Say you need to validate a password in three different places. Copy-paste the logic three times, and the day you tighten your security rules you have to remember to change all three. Miss one, and you've shipped a bug.
Today we learn to package logic into functions. By the end we'll have built a modular password generator and security analyzer built from reusable blocks, default parameters, and clean scope.
Here's the target:
--- Security Tool ---
[1] Generating secure password (length: 15)...
Result: kX9#mP2$vL0!qR7
[2] Analyzing custom password: 'admin'
Result: WEAK - Too short. Must be at least 8 characters.
[3] Analyzing custom password: 'SuperSecretPassword123'
Result: MODERATE - Add both numbers and special characters for max security.
Let's stop writing scripts and start writing software.
🛑 Dev Callout: The "Block Scope" Trap
Coming from C#, C++, or Java? Pay attention here, because this one bites. In those languages a variable declared inside an
ifor afordies when that block ends — that's block scope.Python has no block scope. It has function scope and global scope, and that's it.
if True: temp_token = "ABC" print(temp_token) # Works fine in Python. Would not compile in Java.A variable created inside a loop or an
ifleaks into the rest of the enclosing function. It's not a bug, it's the design — but it means a name you thought was temporary can quietly collide with something later. The habit that saves you: keep your variables inside functions (not floating at the top of the file), and give them names specific enough that two of them never fight over the same identifier.
1. Defining Your First Function (def)
A function is a named, reusable block of code. You define it with the def keyword, a name in snake_case, parentheses (), and a colon :.
def boot_sequence():
print("Initializing core modules...")
print("Loading user preferences...")
print("System ready.")
# Defining it doesn't run it. Calling it does:
boot_sequence()
When Python reads a def block, it doesn't execute the body — it just memorizes it. The code only runs when you call the function by name with ().
2. Parameters and Arguments
Functions get useful when they take input. The names in the definition are parameters; the actual values you pass in are arguments.
def ban_user(username, reason):
print(f"User {username} has been banned. Reason: {reason}")
ban_user("hacker99", "Exploiting bugs")
Default parameters. Sometimes a parameter should be optional. Give it a default value in the definition; callers can override it or leave it alone:
def ping_server(ip_address, retries=3):
print(f"Pinging {ip_address}... (max retries: {retries})")
ping_server("192.168.1.1") # uses the default, 3
ping_server("10.0.0.5", retries=10) # overrides it
One rule: parameters with defaults must come after parameters without them in the definition. Python won't let you put a required parameter after an optional one.
3. The return Statement
Beginners constantly mix up print() and return. They are not the same thing:
print()dumps text to the console for a human to read.returnhands a value back to the program so other code can use it.
The moment a function hits return, it exits.
def calculate_tax(subtotal):
tax_rate = 0.08
total = subtotal + (subtotal * tax_rate)
return total # hand the number back to whoever called us
# Capture the returned value in a variable:
checkout_price = calculate_tax(50.00)
print(f"Please pay: ${checkout_price:.2f}")
If a function has no return, it implicitly returns None — Python's equivalent of null.
4. Understanding Scope (The LEGB Rule)
Scope is the answer to "which variables can this function see?" Python resolves a name by looking, in order:
Local — names created inside the function itself.
Enclosing — names in an outer function, if this one is nested inside another.
Global — names defined at the top level of the file.
Built-in — Python's pre-loaded names, like
printandlen.
system_status = "ONLINE" # global
def check_system():
status_code = 200 # local
print(f"System is {system_status} with code {status_code}")
check_system()
# print(status_code) # 💥 NameError — locals are destroyed when the function returns
A warning about global. A function can read a global variable, but it can't reassign one by default — try, and Python just makes a new local of the same name. There's a global keyword that forces it, but don't reach for it. Functions that mutate global state are hard to test and hard to reason about. The clean pattern is the one functions were built for: take data in through parameters, send results out through return.
5. Bringing It Together: The Security Analyzer
Let's build the tool. Two functions, each doing one job: one generates passwords, the other rates them. Notice how short and readable the actual execution part at the bottom becomes once the logic lives in functions.
Create a file named security_tool.py:
import random
import string
# --- FUNCTION DEFINITIONS ---
def generate_password(length=12):
"""Generate a random password of the given length."""
# string.ascii_letters -> a-z and A-Z
# string.digits -> 0-9
# string.punctuation -> !@#$%^&* and friends
characters = string.ascii_letters + string.digits + string.punctuation
password = ""
for _ in range(length): # _ is the conventional name for "I don't need this value"
password += random.choice(characters)
return password
def analyze_strength(password):
"""Rate a password and return a human-readable verdict."""
if len(password) < 8:
return "WEAK - Too short. Must be at least 8 characters."
# any() returns True if at least one item passes the test.
has_number = any(char.isdigit() for char in password)
has_special = any(char in string.punctuation for char in password)
if not has_number and not has_special:
return "WEAK - Missing numbers and special characters."
elif not has_number or not has_special:
return "MODERATE - Add both numbers and special characters for max security."
else:
return "STRONG - Meets all security criteria."
# --- MAIN SCRIPT ---
print("--- Security Tool ---\n")
print("[1] Generating secure password (length: 15)...")
new_pass = generate_password(length=15)
print(f"Result: {new_pass}\n")
print("[2] Analyzing custom password: 'admin'")
print(f"Result: {analyze_strength('admin')}\n")
print("[3] Analyzing custom password: 'SuperSecretPassword123'")
print(f"Result: {analyze_strength('SuperSecretPassword123')}")
The text between the triple quotes right under each def is a docstring — Python's built-in way to document what a function does. Your editor will show it on hover, and tools like help() read it. Get in the habit now.
What's Next?
You've graduated from messy scripts to modular logic. You can write functions, pass data around cleanly, and keep your global namespace tidy.
But everything our tool produces vanishes the instant the program closes. What if we want to save a generated password? Or read a list of 1,000 weak passwords from a file and analyze them in bulk? In Part 5: Error Handling & Modern File Operations, we'll learn to read and write files the modern way — and to stop our programs from crashing when reality inevitably misbehaves.