Python Notes - Part 4: The
Real-World Toolkit
The Practical Skills Behind Bigger Python Projects
Covers core syntax, data types, control flow, functions, object-oriented
programming, file handling and two hands-on mini projects -- with runnable
code and sample output for every topic.
1
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
Table of Contents
1. What a Variable Really Is: Labels, Not Boxes
2. Making a True Independent Copy: The copy Module
3. Building Your Own 'For-Loopable' Object
4. Ready-Made Looping Tricks: The itertools Module
5. Smarter Containers: The collections Module
6. Making Printed Numbers Look Professional
7. When One Parent Class Isn't Enough: Multiple Inheritance
8. Forcing a Rule: 'Every Subclass MUST Have This Method'
9. Giving Your Objects Their Own Comparison and Indexing Rules
10. Inventing Your Own Error Types
11. Building Command-Line Tools People Can Actually Use
12. Keeping Secrets Out of Your Code: Environment Variables
13. Splitting a Big Project Into Several Files (Packages)
14. Everyday File Chores: Moving, Copying and Zipping
15. Fetching Real Data From the Internet
16. Finding a Bug Without Guessing: A Gentle Intro to Debugging
17. Practical Project: Tidy My Downloads Folder
18. Practical Project: A Small Inventory System With Custom Rules
19. A Map of the Wider Python World
2
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
1. What a Variable Really Is: Labels, Not Boxes
Earlier notes compared a variable to a labelled jar holding a value. That picture is useful for beginners,
but here is the more accurate version: a variable is really just a name tag stuck onto a value that lives
somewhere in the computer's memory. Two name tags can point at the exact same value at the same
time.
list_a = [1, 2, 3]
list_b = list_a # list_b is just another name tag on the SAME list
list_b.append(4)
print(list_a) # changed too, even though we only touched list_b!
print(list_a is list_b) # True -- they point to the same thing in memory
Output:
[1, 2, 3, 4]
True
Why Numbers and Text Don't Have This Surprise
Numbers, text and True/False values are 'immutable' -- once created they can never be changed, only
replaced. So reassigning them always creates a fresh value rather than quietly changing a shared one.
x = 5
y = x
y = y + 1
print(x, y) # x is untouched, because y+1 made a brand new number
Output:
5 6
Note: This single idea -- mutable things (lists, dicts, sets, and your own class objects) can be changed
'from underneath you' through another name tag, while immutable things (numbers, text, tuples) cannot
-- explains a huge number of confusing bugs beginners run into.
3
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
2. Making a True Independent Copy: The copy Module
Because assignment just adds another name tag, sometimes you genuinely want a separate,
independent duplicate. Python's copy module gives you two levels of duplication.
import copy
original = [1, 2, [99, 100]]
shallow = [Link](original)
deep = [Link](original)
shallow[0] = "changed"
deep[0] = "changed"
original[2].append("added") # change something nested
print("original:", original)
print("shallow:", shallow)
print("deep:", deep)
Output:
original: [1, 2, [99, 100, 'added']]
shallow: ['changed', 2, [99, 100, 'added']]
deep: ['changed', 2, [99, 100]]
Note: A shallow copy duplicates the outer list, but any list-inside-a-list is still shared. A deep copy
duplicates everything, all the way down, so nothing is shared at all. Use deepcopy when you have
nested lists or dictionaries and want a completely separate version.
4
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
3. Building Your Own 'For-Loopable' Object
Earlier notes used yield to build a generator quickly. Underneath, every for loop is actually asking an
object two questions over and over: 'give me your next-in-line helper' and then 'what comes next?'. You
can build this by hand using two special methods.
class EvenNumbers:
def __init__(self, up_to):
self.up_to = up_to
def __iter__(self):
[Link] = 0
return self
def __next__(self):
if [Link] > self.up_to:
raise StopIteration
value = [Link]
[Link] += 2
return value
for number in EvenNumbers(10):
print(number)
Output:
0
2
4
6
8
10
Note: raise StopIteration is how an iterator says 'I am finished' -- the for loop is quietly watching for
exactly that signal to know when to stop.
5
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
4. Ready-Made Looping Tricks: The itertools Module
itertools is a toolbox of clever, memory-friendly looping helpers so you don't have to write nested loops
by hand for common patterns.
import itertools
sizes = ["S", "M", "L"]
colours = ["Red", "Blue"]
# every possible combination of a size with a colour
for combo in [Link](sizes, colours):
print(combo)
Output:
('S', 'Red')
('S', 'Blue')
('M', 'Red')
('M', 'Blue')
('L', 'Red')
('L', 'Blue')
A Few More Handy Ones
import itertools
# every 2-person pairing from a group, order doesn't matter
for pair in [Link](["Alice", "Bob", "Cara"], 2):
print(pair)
# join two lists into one continuous sequence
for item in [Link]([1, 2], [3, 4]):
print(item, end=" ")
Output:
('Alice', 'Bob')
('Alice', 'Cara')
('Bob', 'Cara')
1 2 3 4
6
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
5. Smarter Containers: The collections Module
The collections module offers upgraded versions of lists and dictionaries for very common situations,
saving you from writing boilerplate 'if key exists, otherwise...' code.
defaultdict -- A Dictionary With a Built-in Default
from collections import defaultdict
groups = defaultdict(list)
students = [("Team A", "Ravi"), ("Team B", "Sara"), ("Team A", "Kunal")]
for team, name in students:
groups[team].append(name) # no need to check "if team not in groups" first
print(dict(groups))
Output:
{'Team A': ['Ravi', 'Kunal'], 'Team B': ['Sara']}
namedtuple -- A Tiny Read-Only Record With Named Fields
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 7)
print(p.x, p.y)
print(p)
Output:
3 7
Point(x=3, y=7)
deque -- A List That's Fast at Both Ends
from collections import deque
queue = deque(["first", "second", "third"])
[Link]("fourth") # add to the back
[Link]("zeroth") # add to the front
print([Link]()) # remove from the front
print(queue)
Output:
zeroth
deque(['first', 'second', 'third', 'fourth'])
7
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
6. Making Printed Numbers Look Professional
f-strings can do much more than drop a value into a sentence -- a format specifier after a colon controls
decimal places, minimum width, padding and thousands separators.
price = 2500000
pi_value = 3.14159265
name = "Sam"
print(f"{price:,}") # thousands separator
print(f"{pi_value:.2f}") # exactly 2 decimal places
print(f"{name:>10}|") # right-align in a 10-character space
print(f"{name:<10}|") # left-align in a 10-character space
print(f"{7:03}") # pad with zeros to 3 digits
Output:
2,500,000
3.14
Sam|
Sam |
007
Percentages Made Easy
success_rate = 0.874
print(f"Success rate: {success_rate:.1%}")
Output:
Success rate: 87.4%
8
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
7. When One Parent Class Isn't Enough: Multiple
Inheritance
A class can inherit abilities from more than one parent at the same time, similar to a child inheriting
traits from both parents rather than just one. Small, focused parent classes used this way are often
called 'mixins'.
class SwimmerMixin:
def swim(self):
print(f"{[Link]} is swimming")
class RunnerMixin:
def run(self):
print(f"{[Link]} is running")
class Triathlete(SwimmerMixin, RunnerMixin):
def __init__(self, name):
[Link] = name
athlete = Triathlete("Meera")
[Link]()
[Link]()
Output:
Meera is swimming
Meera is running
Note: When two parent classes both define the same method name, Python follows a fixed search
order (left to right, called the Method Resolution Order) to decide which one wins -- worth knowing
exists, but rarely something you need to think about in everyday code.
9
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
8. Forcing a Rule: 'Every Subclass MUST Have This
Method'
Sometimes you want to write a parent class as a template that says 'anyone who inherits from me
MUST provide their own version of this method' -- like a franchise agreement that insists every branch
must offer delivery, even though head office doesn't specify how.
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def pay(self, amount):
pass
class CreditCard(PaymentMethod):
def pay(self, amount):
print(f"Charged {amount} to credit card")
card = CreditCard()
[Link](500)
# PaymentMethod() -> this line would crash: you cannot create the template itself
# class Wallet(PaymentMethod): pass -> this would also crash: pay() was never provided
Output:
Charged 500 to credit card
10
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
9. Giving Your Objects Their Own Comparison and
Indexing Rules
Earlier notes taught __str__, __eq__ and __add__. Two more dunder methods are worth knowing: one
for < and > comparisons, and one that lets your object be accessed with square brackets like a list.
class Product:
def __init__(self, name, price):
[Link] = name
[Link] = price
def __lt__(self, other):
return [Link] < [Link]
def __repr__(self):
return f"{[Link]} (Rs.{[Link]})"
items = [Product("Pen", 20), Product("Bag", 800), Product("Book", 250)]
print(sorted(items)) # sorting now works, because Python knows how to compare them
Output:
[Pen (Rs.20), Book (Rs.250), Bag (Rs.800)]
Square-Bracket Access With __getitem__
class Playlist:
def __init__(self, songs):
[Link] = songs
def __getitem__(self, index):
return [Link][index]
def __len__(self):
return len([Link])
my_playlist = Playlist(["Song A", "Song B", "Song C"])
print(my_playlist[1])
print(len(my_playlist))
Output:
Song B
3
11
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
10. Inventing Your Own Error Types
Built-in errors like ValueError are general-purpose. In a real project, it often reads much more clearly to
invent your own named error for a specific problem in your program, the same way a hospital has
different named alarms instead of one generic beep for every emergency.
class InsufficientFundsError(Exception):
pass
class BankAccount:
def __init__(self, balance):
[Link] = balance
def withdraw(self, amount):
if amount > [Link]:
raise InsufficientFundsError(f"Cannot withdraw {amount}, balance is only {s
[Link]}")
[Link] -= amount
account = BankAccount(1000)
try:
[Link](5000)
except InsufficientFundsError as e:
print("Transaction blocked:", e)
Output:
Transaction blocked: Cannot withdraw 5000, balance is only 1000
12
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
11. Building Command-Line Tools People Can Actually
Use
Earlier notes read a single value from [Link]. For a real command-line tool with named options (like
--name Alice --times 3), the argparse module builds a proper interface, complete with a free --help
screen.
# save as [Link]
import argparse
parser = [Link](description="Greet someone, repeatedly.")
parser.add_argument("--name", required=True, help="Name of the person to greet")
parser.add_argument("--times", type=int, default=1, help="How many times to greet")
args = parser.parse_args()
for _ in range([Link]):
print(f"Hello, {[Link]}!")
# Run from the terminal like this:
# python3 [Link] --name Priya --times 3
Output:
Hello, Priya!
Hello, Priya!
Hello, Priya!
13
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
12. Keeping Secrets Out of Your Code: Environment
Variables
Passwords, API keys and other secrets should never be typed directly into your source code,
especially if that code will ever be shared or uploaded anywhere. Environment variables let the secret
live outside the code, in the operating system itself.
# in the terminal, before running your program:
# export API_KEY="abc123secret" (Mac/Linux)
# set API_KEY=abc123secret (Windows)
import os
api_key = [Link]("API_KEY", "no key found")
print(f"Using key: {api_key}")
Output:
Using key: abc123secret
Note: [Link](name, default) is safer than [Link][name] because it won't crash your
program if the variable was never set -- it just falls back to whatever default you provide.
14
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
13. Splitting a Big Project Into Several Files (Packages)
As a project grows, keeping everything in one giant file becomes hard to navigate. Python lets you split
code into several files (modules) inside a folder, and treat that whole folder as one importable package.
my_shop/
__init__.py
[Link]
[Link]
[Link]
# [Link]
def apply_tax(amount, tax_rate=0.18):
return amount + (amount * tax_rate)
# [Link]
from my_shop import pricing
print(pricing.apply_tax(1000))
Output:
1180.0
Note: The empty __init__.py file is what tells Python 'this folder is a package, not just a random folder
of scripts'. In modern Python it can often be left completely empty.
15
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
14. Everyday File Chores: Moving, Copying and Zipping
The shutil module (short for 'shell utilities') handles the kind of file chores you'd normally do by dragging
icons around in a file explorer -- copying, moving, deleting whole folders, and zipping things up.
import shutil
[Link]("[Link]", "backup/[Link]")
[Link]("[Link]", "archive/[Link]")
# compress a whole folder into a single .zip file
shutil.make_archive("project_backup", "zip", "my_shop")
Note: shutil.make_archive creates project_backup.zip containing everything inside the my_shop folder,
which is a quick way to back up or share a small project.
16
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
15. Fetching Real Data From the Internet
Earlier notes used the requests library to check a website was online. The same library can fetch real
data for you to use, most commonly in JSON format, which you already know how to read from an
earlier chapter.
import requests
response = [Link]("[Link]
data = [Link]()
print(data["full_name"])
print(f"Stars: {data['stargazers_count']:,}")
Note: Different websites and services expect different URLs and give back different fields -- always
check the specific service's documentation for what's available, since [Link] is just one
example.
17
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
16. Finding a Bug Without Guessing: A Gentle Intro to
Debugging
Sprinkling print() everywhere is a perfectly valid first debugging technique, but Python also ships a
built-in debugger that lets you pause a program mid-run and inspect exactly what every variable holds
at that exact moment.
def calculate_average(numbers):
total = sum(numbers)
breakpoint() # program pauses here, drops you into an interactive prompt
return total / len(numbers)
calculate_average([10, 20, 30])
Note: Once paused, you can type variable names to inspect them, n to move to the next line, or c to
continue running normally -- like pressing pause on a video to look closely at one frame before
pressing play again.
18
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
17. Practical Project: Tidy My Downloads Folder
A genuinely useful little script: it looks at every file in a folder and sorts each one into a subfolder
named after its file type.
from pathlib import Path
import shutil
FOLDER_TYPES = {
".jpg": "Images", ".png": "Images",
".pdf": "Documents", ".docx": "Documents",
".mp3": "Audio", ".mp4": "Video",
}
def organise_folder(folder_path):
folder = Path(folder_path)
for file in [Link]():
if file.is_file():
category = FOLDER_TYPES.get([Link](), "Other")
destination_folder = folder / category
destination_folder.mkdir(exist_ok=True)
[Link](str(file), str(destination_folder / [Link]))
print(f"Moved {[Link]} -> {category}/")
# organise_folder("Downloads")
Note: The line has been commented out on purpose so this notes file doesn't accidentally move
anything -- remove the # to actually run it on a real folder on your own computer.
19
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
18. Practical Project: A Small Inventory System With
Custom Rules
This project ties together custom exceptions, dunder methods and dataclass-style records into a tiny
but realistic inventory manager.
class OutOfStockError(Exception):
pass
class InventoryItem:
def __init__(self, name, quantity, price):
[Link] = name
[Link] = quantity
[Link] = price
def __repr__(self):
return f"{[Link]}: {[Link]} @ Rs.{[Link]}"
def sell(self, count):
if count > [Link]:
raise OutOfStockError(f"Only {[Link]} left of {[Link]}")
[Link] -= count
return count * [Link]
stock = {
"notebook": InventoryItem("Notebook", 10, 60),
"pen": InventoryItem("Pen", 3, 20),
}
print(stock["notebook"].sell(4))
try:
stock["pen"].sell(10)
except OutOfStockError as e:
print("Sale failed:", e)
print(stock["notebook"])
Output:
240
Sale failed: Only 3 left of Pen
Notebook: 6 @ Rs.60
20
Python Notes - Part 4: The Real-World Toolkit: A Practical Guide
19. A Map of the Wider Python World
With the fundamentals from all four parts in hand, here's roughly where different areas of real-world
Python work live, so you know what to search for next based on what you want to build.
● Websites and APIs: Flask (small/simple) or Django (full-featured) for the server side.
● Data analysis and spreadsheets at scale: pandas, openpyxl, numpy.
● Graphs and visualisations: matplotlib, plotly.
● Automating repetitive tasks: the very topics in this notes series, plus scheduling tools like cron.
● Machine learning and AI: scikit-learn for classic ML, PyTorch or TensorFlow for deep learning.
● Desktop apps with buttons and windows: tkinter (built in) or PyQt.
● Talking to databases: sqlite3 (built in, file-based) or SQLAlchemy for bigger databases.
Note: You do not need to learn all of these. Pick the single area that matches something you actually
want to build, and learn just enough of that library to finish that one project -- depth comes from
finishing real things, not from reading library documentation end to end.
21