Python My Course#
The high-yield Python course reference: common everyday syntax, core collections, functions, and practical problem solutions.
Daily Syntax & Logic#
The daily syntax: declaring variables, converting types, manipulating text, evaluating conditions, and writing basic loops.
Variables, Types & Casting#
Theory#
In Python, variables are names that point to values in memory. You don't specify variable types when declaring them; Python figures out the type automatically at runtime. Converting between types is done explicitly with functions like str(), int(), float(), and bool().
A variable is simply a sticky note label you slap onto a jar. If you write count = 5, the label count is stuck to a jar holding the number 5. You can move that label to another jar anytime (count = "five").
# Everyday variable assignment
user_name = "Alex"
user_age = 28
user_score = 94.5
is_active = True
session_token = None
# Type conversion
input_str = "150"
total = int(input_str) + 50
print(total) # 200
# Recommended type checking
if isinstance(total, int):
print("total is an integer")
- Strength — Fast Prototyping: Dynamic typing lets you assign and manipulate values without repetitive type declarations.
- Weakness — Runtime Type Mismatches: Adding incompatible types like
"Score: " + 10raises aTypeErrorat runtime instead of converting automatically.
Key idea: To check an object's type, always use isinstance(obj, ExpectedType) rather than comparing type(obj) == ExpectedType.
Questions#
Given a mixed list of values containing numbers, numeric strings, and non-numeric strings, compute the sum of all valid integers.
Iterate over the list, try converting each value to an integer using int(), and add valid integers to a running total while ignoring invalid inputs.
def sum_valid_integers(items: list) -> int:
total = 0
for item in items:
try:
total += int(item)
except (ValueError, TypeError):
continue
return total
# Test cases
assert sum_valid_integers([10, "20", "abc", 5, None, "30"]) == 65
assert sum_valid_integers(["1", "2", "3"]) == 6
print("Sum valid integers passed!")
Everyday String Operations#
Theory#
Strings are one of the most commonly used data types in Python. Python strings are immutable (they cannot be changed in place; operations return a new string). Formatted string literals (f-strings) are the modern, standard way to embed variables and expressions inside text.
An f-string f"Hello {name}, your total is ${price:.2f}" is like a printed letter with blank brackets. Python reads the variables, formats them directly into the blanks, and produces the final letter in a single pass.
# Modern f-strings
product = "Coffee Mug"
price = 12.994
print(f"Item: {product} | Cost: ${price:.2f}") # Item: Coffee Mug | Cost: $12.99
# Cleaning user inputs
raw_email = " Alex@Company.COM \n"
clean_email = raw_email.strip().lower()
print(clean_email) # alex@company.com
# Splitting and joining
csv_row = "laptop,electronics,999"
parts = csv_row.split(",")
print(parts) # ['laptop', 'electronics', '999']
joined = " -> ".join(parts)
print(joined) # laptop -> electronics -> 999
- Strength — Clean F-strings: F-strings can evaluate expressions inline (like
f"{total * 1.08:.2f}") cleanly and quickly. - Weakness — None Method Calls: Calling
.strip()or.lower()on a variable that happens to beNonethrows anAttributeError.
Gotcha: When splitting on whitespace, use s.split() without arguments. It automatically strips outer spaces and collapses multiple spaces between words.
Questions#
Write a function that normalizes a full name by removing extra spaces and capitalizing each word.
Use s.split() to automatically split words by any amount of consecutive whitespace, title-case each word with .capitalize(), and re-join with a single space.
def normalize_name(raw_name: str) -> str:
words = raw_name.split()
return " ".join(word.capitalize() for word in words)
# Test cases
assert normalize_name(" john DOE ") == "John Doe"
assert normalize_name("alice") == "Alice"
assert normalize_name(" ") == ""
print("Normalize name passed!")
Operators, Truthiness & Conditionals#
Theory#
Python uses standard comparison operators (==, !=, <, >) and logical operators (and, or, not). Every object in Python has a boolean value: empty lists, empty dictionaries, empty strings, 0, and None evaluate to False; populated collections and non-zero numbers evaluate to True.
When Python asks if items:, it simply checks if the box has anything inside it. If the box is empty ([], "", {}), Python sees it as empty (False). If there is even one item inside, it sees it as populated (True).
# Truthiness in if statements
cart = []
if not cart:
print("Your cart is empty.")
# Checking None safely
current_user = None
if current_user is None:
print("Please log in.")
# Ternary expression
score = 82
result = "Pass" if score >= 60 else "Fail"
print(result) # Pass
# Checking membership
admin_users = ["alice", "bob"]
if "alice" in admin_users:
print("Access granted.")
- Strength — Idiomatic Emptiness Checks:
if items:andif not items:work identically across strings, lists, tuples, and dictionaries. - Weakness — Default with or Trap: Using
x = val or 10will override0because0is falsy (0 or 10returns10).
Key idea: If a value can legitimately be 0 or False, do not use val = input_val or default. Use val = input_val if input_val is not None else default.
Questions#
Given an integer, return "Fizz" if it's divisible by 3, "Buzz" if divisible by 5, "FizzBuzz" if divisible by both, or the number as a string otherwise.
Check divisibility by both 3 and 5 first (using % 15 == 0), then check 3 and 5 individually.
def fizz_buzz(n: int) -> str:
if n % 15 == 0:
return "FizzBuzz"
elif n % 3 == 0:
return "Fizz"
elif n % 5 == 0:
return "Buzz"
else:
return str(n)
# Test cases
assert fizz_buzz(15) == "FizzBuzz"
assert fizz_buzz(9) == "Fizz"
assert fizz_buzz(10) == "Buzz"
assert fizz_buzz(7) == "7"
print("FizzBuzz passed!")
Loops & Iteration#
Theory#
Python loops iterate directly over items in a collection, rather than keeping track of an incrementing index variable. When you need the index alongside the item, enumerate() gives you both cleanly.
In older languages, running a loop is like saying: "Look at seat 0, who is there? Now look at seat 1, who is there?" In Python, the loop is like reading down the attendee roster directly: "Alice, Bob, Charlie."
fruits = ["apple", "banana", "orange"]
# Direct iteration
for fruit in fruits:
print(fruit)
# Iteration with index
for rank, fruit in enumerate(fruits, start=1):
print(f"{rank}. {fruit}")
# 1. apple
# 2. banana
# 3. orange
# Parallel iteration with zip
prices = [1.20, 0.50, 0.80]
for fruit, price in zip(fruits, prices):
print(f"{fruit}: ${price:.2f}")
# While loop for conditions
count = 3
while count > 0:
print(f"Countdown: {count}")
count -= 1
- Strength — Clean Syntax: Direct iteration prevents off-by-one errors and out-of-bounds indexing bugs.
- Weakness — Modifying While Looping: Removing items from a list while iterating over it will cause the loop to skip subsequent elements.
Key idea: Avoid writing for i in range(len(items)): items[i]. Instead, use for item in items: or for i, item in enumerate(items):.
Questions#
Given a list of numbers, find the index of the first negative number, or return -1 if none exists.
Use enumerate() to inspect both index and number, and return idx as soon as a negative value is encountered.
def find_first_negative(numbers: list[int]) -> int:
for idx, num in enumerate(numbers):
if num < 0:
return idx
return -1
# Test cases
assert find_first_negative([3, 5, -2, 8]) == 2
assert find_first_negative([1, 2, 3]) == -1
assert find_first_negative([-10, 0, 10]) == 0
print("Find first negative passed!")
Core Data Collections#
The primary data structures used every day: ordered lists, fast key-value dictionaries, unique sets, and readable list comprehensions.
Lists & Slicing#
Theory#
A list is Python's standard ordered, mutable array. You can store any types of items in a list. You can access items by index (starting at 0 for the first item, or -1 for the last item). Slicing allows you to extract sub-portions of a list.
A list is a row of numbered wagons hooked together. Wagon 0 is the front, wagon -1 is the caboose. Hooking an extra wagon to the end (append()) is quick and easy.
todos = ["buy groceries", "pay electric bill", "walk dog"]
# Modifying lists
todos.append("read book") # Adds to end
completed = todos.pop() # Removes "read book"
print(f"Finished: {completed}")
# Slicing
numbers = [10, 20, 30, 40, 50]
print(numbers[1:3]) # [20, 30]
print(numbers[:2]) # [10, 20]
print(numbers[-1]) # 50 (last item)
print(numbers[::-1]) # [50, 40, 30, 20, 10] (reversed)
# Membership check
if "buy groceries" in todos:
print("Don't forget the milk!")
- Strength — Flexible & General Purpose: Lists can grow, shrink, and hold mixed data types effortlessly.
- Weakness — Linear Search Speed: Checking
item in my_listscans elements one by one; if you have thousands of items, use asetfor instant lookups.
Key idea: Slicing a list (lst[:]) creates a new shallow copy. Mutating the copy will not affect the original list.
Questions#
Given a list of numbers, return a new list with all duplicate values removed while preserving the original order of first appearance.
Keep a seen set to track items already added, and build a new list appending items only when they haven't been seen yet.
def deduplicate_preserve_order(items: list) -> list:
seen = set()
result = []
for item in items:
if item not in seen:
seen.add(item)
result.append(item)
return result
# Test cases
assert deduplicate_preserve_order([1, 2, 2, 3, 1, 4]) == [1, 2, 3, 4]
assert deduplicate_preserve_order(["a", "b", "a"]) == ["a", "b"]
print("Deduplicate passed!")
Dictionaries#
Theory#
A dict (dictionary) stores data in key-value pairs. Looking up a value by its key is instant (O(1)) regardless of whether the dictionary has 10 items or 1,000,000 items. Keys must be unique and immutable (like strings or numbers).
Instead of flipping through everyone's phone numbers in order, you type "Sarah" and her contact details appear immediately. The contact name is the key; the phone number and address are the values.
user = {
"name": "Sarah Connor",
"role": "Engineer",
"level": 3
}
# Safe lookup with .get()
location = user.get("location", "Remote")
print(f"Location: {location}") # Remote
# Adding and updating
user["level"] = 4
user["team"] = "DevOps"
# Iterating over key-value pairs
for key, value in user.items():
print(f"{key}: {value}")
- Strength — Instant Key Lookups: Dictionaries are the fastest and most natural way to structure associated data and configuration mappings.
- Weakness — Direct Bracket KeyError: Accessing
user["missing_key"]directly crashes with aKeyError; useuser.get("missing_key")when a key might be absent.
Key idea: Always use d.get(key, fallback) when reading optional fields or external data to avoid unexpected KeyError crashes.
Questions#
Given a list of words, count how many times each word appears and return a dictionary of word counts.
Iterate over the words and update a frequency count dictionary using .get(word, 0) + 1.
def count_word_frequencies(words: list[str]) -> dict[str, int]:
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
return counts
# Test cases
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
result = count_word_frequencies(words)
assert result == {"apple": 3, "banana": 2, "orange": 1}
print("Word frequency passed!")
Tuples & Sets#
Theory#
A tuple is an ordered, immutable sequence written with parentheses (1, 2). Once created, you cannot add, remove, or modify items in a tuple. A set is an unordered collection of unique elements written with curly braces {1, 2}. Sets automatically eliminate duplicates and offer instant O(1) membership checks.
A GPS location (latitude, longitude) is a tuple: latitude and longitude are permanently paired together; changing one without the other makes no sense. A security gate scanner is a set: it holds a list of authorized employee badge IDs; duplicate scans are ignored, and checking if an ID is valid takes a fraction of a second.
# Tuples for fixed records and unpacking
user_coordinate = (37.7749, -122.4194)
lat, lon = user_coordinate
print(f"Latitude: {lat}, Longitude: {lon}")
# Swapping two variables with tuple packing
a = 5
b = 10
a, b = b, a
print(a, b) # 10 5
# Sets for unique elements
raw_roles = ["admin", "editor", "admin", "viewer", "editor"]
unique_roles = set(raw_roles)
print(unique_roles) # {'admin', 'editor', 'viewer'}
# Fast membership check
if "admin" in unique_roles:
print("Admin role found!")
- Strength — Automatic Deduplication & Speed: Converting a list to a set strips duplicates instantly, and
val in setis much faster thanval in list. - Weakness — Unordered Sets: Sets do not preserve order; if you need both order and uniqueness, use a list alongside a set.
Gotcha: Writing x = {} creates an empty dictionary, not an empty set. To create an empty set, you must write x = set().
Questions#
Given two lists of user IDs, return a list of all user IDs that appear in both lists.
Convert both lists to sets and take their intersection using the & operator.
def find_common_users(group_a: list[str], group_b: list[str]) -> list[str]:
return list(set(group_a) & set(group_b))
# Test cases
users_1 = ["alice", "bob", "charlie"]
users_2 = ["bob", "david", "alice"]
common = find_common_users(users_1, users_2)
assert set(common) == {"alice", "bob"}
print("Common users passed!")
List Comprehensions#
Theory#
A list comprehension provides a concise, readable way to create a new list by transforming or filtering elements from an existing collection. It replaces verbose multi-line for loops that append items one by one.
A standard for loop is like an inspector inspecting each widget, deciding if it meets standards, and walking it over to place it into a bin. A list comprehension is a conveyor belt with a built-in filter: pieces that pass the filter drop directly into the box in one smooth motion.
# Example 1: Squaring numbers
numbers = [1, 2, 3, 4, 5]
squares = [n * n for n in numbers]
print(squares) # [1, 4, 9, 16, 25]
# Example 2: Filtering even numbers
evens = [n for n in numbers if n % 2 == 0]
print(evens) # [2, 4]
# Example 3: Cleaning strings
raw_tags = [" python ", "FASTAPI", " sql "]
clean_tags = [tag.strip().lower() for tag in raw_tags]
print(clean_tags) # ['python', 'fastapi', 'sql']
- Strength — Clean & Expressive: Turns repetitive 4-line accumulator loops into a single self-explanatory statement.
- Weakness — Avoid Overcomplicating: Don't write nested comprehensions spanning three lines with multiple
ifclauses; write a regularforloop when logic gets complex.
Key idea: Only use list comprehensions when you intend to produce a new list. Never use a comprehension solely to produce side effects (like [print(x) for x in items]); use a normal for loop instead.
Questions#
Given a list of file names, return a list containing only the .csv files converted to lowercase.
Use a list comprehension filtering with .endswith(".csv") and transforming with .lower().
def get_csv_files(filenames: list[str]) -> list[str]:
return [name.lower() for name in filenames if name.lower().endswith(".csv")]
# Test cases
files = ["data.CSV", "report.pdf", "users.csv", "image.png"]
assert get_csv_files(files) == ["data.csv", "users.csv"]
print("Filter CSV files passed!")
Functions & Error Handling#
Writing clean reusable functions, handling arguments, and catching runtime errors gracefully.
Functions & Arguments#
Theory#
Functions let you package code into reusable blocks. Functions take arguments, execute logic, and return values using the return statement. If a function doesn't include an explicit return, it returns None automatically.
A function is like a blender with predefined settings. You drop ingredients into the top (arguments), press the blend button (execute function), and pour out the resulting smoothie (return value).
# Function with default parameters
def calculate_total(subtotal: float, tax_rate: float = 0.08) -> float:
return round(subtotal * (1 + tax_rate), 2)
print(calculate_total(100.0)) # 108.0 (uses default 8% tax)
print(calculate_total(100.0, 0.05)) # 105.0 (overrides tax rate)
# Returning multiple values
def get_min_and_max(numbers: list[int]):
return min(numbers), max(numbers)
lowest, highest = get_min_and_max([12, 45, 2, 89, 23])
print(f"Min: {lowest}, Max: {highest}") # Min: 2, Max: 89
- Strength — Reusability & Modularity: Breaking code into focused, well-named functions makes testing and debugging straightforward.
- Weakness — Mutable Default Argument Trap: Never use a mutable list or dictionary as a default argument (like
def fn(items=[])); useitems=Noneand initialize inside the function.
Key idea: To use an optional list parameter in a function, write def my_func(items=None): if items is None: items = [].
Questions#
Write a function that calculates a bill's tip amount based on bill total and a percentage, defaulting to 15% tip.
Define a function taking bill: float and percentage: float = 15.0, computing bill * (percentage / 100).
def calculate_tip(bill: float, percentage: float = 15.0) -> float:
if bill < 0:
raise ValueError("Bill cannot be negative.")
return round(bill * (percentage / 100), 2)
# Test cases
assert calculate_tip(100.0) == 15.0
assert calculate_tip(50.0, 20.0) == 10.0
assert calculate_tip(0.0) == 0.0
print("Calculate tip passed!")
Everyday Error Handling#
Theory#
Errors happen: an API returns bad text, a user types letters into a number field, or a file doesn't exist. Python uses try / except blocks so your program handles errors gracefully instead of crashing abruptly.
The code inside try: is the gymnast performing high on the trapeze. If they perform the routine smoothly, they finish and dismount. If they slip (an error occurs), the except: safety net catches them safely so nobody gets hurt.
# Catching specific parsing errors
raw_input = "forty-two"
try:
age = int(raw_input)
print(f"Age is {age}")
except ValueError:
print(f"Could not convert '{raw_input}' to an integer. Setting default age to 0.")
age = 0
# Safe dictionary lookups
config = {"timeout": 30}
try:
retries = config["retries"]
except KeyError:
retries = 3
print(f"Using {retries} retries.")
- Strength — Prevents Application Crashes: Catching specific errors lets services log failures and return fallback responses without taking down the server.
- Weakness — Bare except: pass Anti-pattern: Writing
except: passsilences every error, including keyboard interrupts and variable typos, making debugging impossible.
Gotcha: Always specify the error you expect to catch (such as except ValueError:, not just except:).
Questions#
Write a function that safely divides two numbers and returns None if division by zero occurs.
Wrap the division operation a / b inside a try / except ZeroDivisionError block.
def safe_divide(a: float, b: float):
try:
return a / b
except ZeroDivisionError:
return None
# Test cases
assert safe_divide(10, 2) == 5.0
assert safe_divide(5, 0) is None
assert safe_divide(0, 5) == 0.0
print("Safe divide passed!")
🐍 Next steps: Ready for OOP, decorators, generators, and async programming? Continue to the Python Crash Course, or the deep-dive Python Detailed Course for CPython internals. Browse all languages in the Language Courses catalog.