Python Programming From Zero

05

3-Level Ladder

Beginner, intermediate, advanced, with a practical exercise after each level.

Level 1: Beginner

You can write and run a small Python script that stores data in variables, converts between types, formats output, and makes decisions with if statements.

Values, types, and variables

Everything in Python is a value with a type. The main starter types are int for whole numbers (42), float for decimals (3.5), str for text ("hello"), and bool for True and False. A variable is just a name you attach to a value with =, as in price = 9.99. The name goes on the left, the value on the right; nothing is copied or reserved in advance.

You can always ask Python what it is holding. type(price) reports <class 'float'>, and print(price) shows the value. Types matter because they decide what operations mean: 3 * 2 is 6, but "ab" * 2 is "abab". When you mix incompatible types, like "Total: " + 9.99, Python raises a TypeError instead of guessing, which is a feature, not an annoyance.

Input, conversion, and f-strings

input() always hands you a string, even when the user types digits. So age = input("Age: ") followed by age + 1 fails. Convert first: age = int(input("Age: ")). Use int() for whole numbers, float() for decimals, and str() to turn a number back into text.

To build readable output, use an f-string: put an f before the quotes and drop expressions in braces. name = "Ana"; total = 12.5; print(f"{name} owes ${total:.2f}") prints Ana owes $12.50. The :.2f part rounds the display to two decimal places, which is exactly what you want for money on screen.

Decisions with if, elif, and else

An if statement runs a block only when a condition is True. Conditions come from comparisons (==, !=, <, >=) and can be combined with and, or, and not. Note the difference between = (assign) and == (compare); mixing them up is the classic first-week bug.

Indentation is the syntax. The lines under an if must be indented consistently, four spaces by convention, and Python uses that indentation to decide what belongs to the block. A chain runs top to bottom and stops at the first match: if score >= 90: grade = "A" then elif score >= 80: grade = "B" then else: grade = "C". Because it stops at the first true branch, order your conditions from strictest to loosest.

Exercise

Write a script that asks for a meal price and a service rating typed as good, okay, or bad. Tip 20 percent for good, 15 percent for okay, and 10 percent for bad. Print the tip and the total, both formatted to two decimal places, in one sentence.

Hint

Convert the price with float() before doing math, and normalize the rating with .lower().strip() so " Good " still matches. Pick the tip rate with an if/elif/else chain, then compute tip = price * rate.

Solution

price = float(input("Meal price: "))

rating = input("Service (good/okay/bad): ").lower().strip()

if rating == "good":

rate = 0.20

elif rating == "okay":

rate = 0.15

else:

rate = 0.10

tip = price * rate

total = price + tip

print(f"Tip ${tip:.2f} on ${price:.2f} for a total of ${total:.2f}.")

A sample run with 40 and good prints: Tip $8.00 on $40.00 for a total of $48.00. If you want bad input rejected instead of defaulting to 10 percent, add a final else that prints a message and stops.

Level 2: Intermediate

You can store collections of data in lists and dictionaries, loop over them safely, and package repeated work into functions with clear inputs and return values.

Lists and loops that do real work

A list holds an ordered, changeable sequence: temps = [18, 22, 19, 25]. You append to grow it, index to read one item, and slice to take a chunk (temps[1:3] gives [22, 19]). The natural way to visit every item is for t in temps, which hands you the values directly. Reach for range only when you truly need positions, and use enumerate(temps) when you need both index and value at once.

One rule saves a lot of pain: do not add to or delete from a list while you are looping over it, because the loop's internal position gets out of sync and items get skipped. Build a new list instead, or loop over a copy with for t in temps[:]. Handy helpers you should know by name are len, sum, min, max, sorted, and the in operator for membership tests like if 22 in temps.

Dictionaries for lookups

A dictionary maps keys to values: stock = {"apples": 12, "pears": 3}. Lookup by key is direct and fast no matter how big the dictionary gets, which is why dictionaries beat lists whenever your question is "what is the value for this name?" Assigning to a missing key creates it, so stock["plums"] = 7 adds an entry.

Reading a missing key raises a KeyError, so use stock.get("plums", 0) when absence is normal and you want a default. Loop with for name, count in stock.items() to get pairs, or for name in stock to get keys. Keys must be immutable, which means strings, numbers, and tuples work but lists do not. A counting loop looks like counts[word] = counts.get(word, 0) + 1, and that one line is the backbone of most tallying code you will write.

Functions, arguments, and return values

A function turns a block of code into a named tool: def average(numbers): return sum(numbers) / len(numbers). Parameters are the names in the definition, arguments are the values you pass in, and return hands a result back to the caller. Prefer returning values over printing inside the function, because a returned value can be tested, stored, and reused, while a print can only be watched.

Names created inside a function are local and vanish when the function ends, so a function cannot accidentally clobber your outer variables unless you go out of your way with global. Default values make functions flexible, as in def greet(name, greeting="Hello"). One caution: never use a mutable default like def add(item, bag=[]), because that list is created once and shared across every call. Use bag=None and create a fresh list inside instead.

Exercise

Write a function word_counts(text) that returns a dictionary mapping each lowercase word to how many times it appears, ignoring case and stripping the punctuation .,!? from the ends of words. Then write a second function top_word(text) that returns the most common word. Test both on "The cat sat. The cat, the mat!"

Hint

Use text.lower().split() to get rough words, then word.strip(".,!?") to clean each one, and skip anything that becomes empty. Build the tally with counts.get(word, 0) + 1. For the top word, use max(counts, key=counts.get).

Solution

def word_counts(text):

counts = {}

for raw in text.lower().split():

word = raw.strip(".,!?")

if not word:

continue

counts[word] = counts.get(word, 0) + 1

return counts

def top_word(text):

counts = word_counts(text)

if not counts:

return None

return max(counts, key=counts.get)

sample = "The cat sat. The cat, the mat!"

print(word_counts(sample))

print(top_word(sample))

This prints {'the': 3, 'cat': 2, 'sat': 1, 'mat': 1} and then the. Notice that word_counts returns data rather than printing it, which is what lets top_word reuse it without duplicating any logic.

Level 3: Advanced

You can read and write files safely, handle errors deliberately, use comprehensions and generators, and organize code into modules and classes that other people can run.

Files and exception handling

Open files with a with block: with open("sales.csv", encoding="utf-8") as f: for line in f: ... The with statement closes the file automatically even if an error is raised mid-loop, and looping over the file object reads one line at a time instead of loading a huge file into memory. Each line keeps its trailing newline, so call line.rstrip("\n") before splitting. To write, open with mode "w" to replace or "a" to append.

Handle errors where you can actually do something about them. Wrap the risky call in try and catch specific exceptions: except FileNotFoundError to report a missing path, except ValueError to skip a row where int(field) fails. A bare except swallows typos and interrupts along with real problems, so name the exception you expect. Use raise to re-throw when the caller is the one who should decide, and else or finally when you need cleanup that always runs.

Comprehensions, generators, and lazy iteration

A comprehension builds a collection in one expression: squares = [n * n for n in range(10) if n % 2 == 0] gives the even squares. The same shape works for dictionaries, {name: len(name) for name in names}, and sets. Comprehensions are clearer than a loop plus append when the body is a single transformation, and harder to read when you start nesting three levels, at which point a plain loop wins.

Swap the brackets for parentheses and you get a generator expression, which produces values one at a time instead of building the whole list. sum(len(line) for line in f) never holds the file in memory. Writing your own generator means using yield inside a function: each yield hands back a value and pauses until the caller asks for the next one. This is how you process a million-row log on a laptop, and why for loops, sum, any, and max can all consume the same lazy stream.

Modules, classes, and making code runnable

Every .py file is a module. import statistics pulls in the standard library, from pathlib import Path pulls in one name, and your own file called tools.py becomes import tools for a script sitting beside it. Guard the script part of a file with if __name__ == "__main__": so that importing it does not run your demo code. Install third-party packages with pip inside a virtual environment created by python -m venv .venv, so each project keeps its own dependency versions.

A class bundles data with the functions that operate on it. def __init__(self, name, balance) sets up each instance's attributes, other methods take self as their first parameter, and __repr__ controls how the object shows up when you print it in a debug session. Reach for a class when several functions keep passing the same cluster of values around; a plain function or a dataclass is enough when you only need to hold fields. Finally, write assertions or small test functions as you go, because code you can rerun in one command is code you can change without fear.

Exercise

You have a file sales.csv whose first line is date,region,amount and whose remaining lines look like 2024-03-01,north,120.50. Write a script that returns a dictionary of total sales per region, skips any row with a missing field or an unparsable amount, reports a clear message if the file does not exist, and prints regions from highest to lowest total.

Hint

Use with open(...) plus next(f) to drop the header. Split each stripped line on commas and check you got three parts. Put float(amount) in its own try/except ValueError so one bad row does not kill the run. Sort with sorted(totals.items(), key=lambda pair: pair[1], reverse=True).

Solution

from pathlib import Path

def totals_by_region(path):

totals = {}

with open(path, encoding="utf-8") as f:

next(f, None) # drop the header

for line in f:

parts = line.strip().split(",")

if len(parts) != 3:

continue

_, region, amount = parts

region = region.strip().lower()

try:

value = float(amount)

except ValueError:

continue

if not region:

continue

totals[region] = totals.get(region, 0.0) + value

return totals

def main():

path = Path("sales.csv")

try:

totals = totals_by_region(path)

except FileNotFoundError:

print(f"No file at {path.resolve()}")

return

for region, total in sorted(totals.items(), key=lambda pair: pair[1], reverse=True):

print(f"{region:<10} {total:>10.2f}")

if __name__ == "__main__":

main()

The reading function returns data and the main function handles input and output, which means you can test totals_by_region on a small temporary file without any printing. Once this works, try replacing the manual splitting with the standard library's csv module and csv.DictReader, which correctly handles quoted fields containing commas.

3-Level Ladder: Python Programming From Zero | WholeTopic