Part 3: Python Programming
| Site: | Learn at Merupu AI |
| Course: | Programming Concepts |
| Book: | Part 3: Python Programming |
| Printed by: | Guest user |
| Date: | Tuesday, 27 October 2026, 7:29 AM |
Description
Part 3: Python Programming — read through each chapter in order. Diagrams, tables and worked examples included.
Table of contents
- 1. Meet Python
- 2. Variables, Values & Dynamic Typing
- 3. Numbers, Operators & Math
- 4. Strings in Python
- 5. Input & Output & Formatting
- 6. Conditionals
- 7. Loops
- 8. Lists & Tuples
- 9. Dictionaries & Sets
- 10. Functions
- 11. Modules & the Standard Library
- 12. File Handling
- 13. Exceptions & Error Handling
- 14. Classes & Objects (OOP)
- 15. Putting It Together
1. Meet Python
Meet Python
Python is a popular, general-purpose programming language created by Guido van Rossum and first released in 1991. It is prized by beginners and professionals alike because its syntax reads almost like plain English. You can use Python to build websites, analyse data, automate boring tasks, control robots, train machine-learning models, and much more. In this chapter you will install Python, learn two ways of running code, and write your very first program.
Why Python?
Three qualities make Python an excellent first language:
- Readable: you write less punctuation and fewer symbols than in many other languages, so you spend more time thinking about ideas and less about syntax.
- Batteries included: Python ships with a large standard library of ready-made tools, so common jobs need very little code.
- Everywhere: it runs on Windows, macOS, and Linux, and a huge community means answers to your questions are easy to find.
Installing Python
Visit python.org and download the latest Python 3 installer for your operating system. On Windows, tick the box that says "Add Python to PATH" before clicking Install. On macOS you can also use the official installer. Once finished, open a terminal (Command Prompt on Windows, Terminal on macOS/Linux) and check the version:
If you see a version number starting with 3, you are ready. (On some systems the command is python3 instead of python.)
The REPL versus a script
There are two common ways to run Python. The first is the REPL (Read-Eval-Print Loop), an interactive prompt where you type one line, press Enter, and immediately see the result. Start it by typing python with no file name. You will see a >>> prompt:
The REPL is perfect for quick experiments. Type exit() to leave it.
The second way is to save your instructions in a script, a plain text file ending in .py. This is how real programs are written, because you can save, edit, and re-run them.
Running a .py file
Create a file called hello.py with one line inside it, then run it from the terminal:
The print() function displays whatever you put between its parentheses. The text in quotation marks is called a string. Printing "Hello, World!" is a tradition for the first program in almost every language.
How a .py file runs
When you type python hello.py, the Python interpreter reads your file line by line, translates it into instructions the computer understands, and runs them. The diagram below shows this journey.
You now have Python installed and have run your first program two different ways. In the next chapters you will learn how to store information, do calculations, and make decisions. Experiment freely: the REPL cannot break your computer, so try typing expressions and see what happens.
2. Variables, Values & Dynamic Typing
Variables, Values & Dynamic Typing
A program is not very useful if it can only work with fixed numbers and text. We need a way to store information and refer to it later. That is what variables are for. A variable is a name that points to a value stored in the computer's memory.
Assignment
You create a variable by writing a name, an equals sign, and a value. This is called assignment:
The equals sign does not mean "equal" as in mathematics. It means "take the value on the right and store it under the name on the left." You can read age = 25 as "age becomes 25."
Changing a variable
A variable can be reassigned at any time. Each new assignment replaces the old value:
Here Python first works out score + 5 (which is 15) and then stores that result back into score.
Naming rules
Variable names must follow a few rules, and some conventions make your code easier to read:
- Names may contain letters, digits, and underscores, but cannot start with a digit.
total2is fine;2totalis an error. - Names are case-sensitive:
Ageandageare two different variables. - You cannot use Python's reserved keywords (such as
if,for,class) as names. - By convention, use lowercase words joined by underscores, like
first_nameortotal_price. This style is called snake_case. - Choose meaningful names.
ntells a reader little;number_of_studentstells them a lot.
Dynamic typing and the type() function
Every value in Python has a type, which describes what kind of data it is. You can ask Python for the type of any value using the built-in type() function:
Python is dynamically typed. This means you never have to declare what type a variable will hold; Python figures it out from the value you assign. It also means the same variable can hold different types at different times:
This flexibility is convenient, but be careful: a variable you think holds a number might have been changed to text elsewhere.
The core built-in types
You will meet these five fundamental types constantly. Here they are with example values.
| Type | Name | Example value | Meaning |
|---|---|---|---|
int | Integer | 25, -3, 0 | Whole numbers, positive or negative |
float | Floating point | 9.99, -0.5, 3.0 | Numbers with a decimal point |
str | String | "Ada", 'hi' | Text, written inside quotes |
bool | Boolean | True, False | A truth value, used for decisions |
NoneType | None | None | "Nothing here" / absence of a value |
Notice that True and False start with capital letters, and None is written with a capital N. These are special values built into the language.
With variables you can now label and reshape data as your program runs. Practice by storing your own name, age, and favourite number in variables, then printing each with its type.
3. Numbers, Operators & Math
Numbers, Operators & Math
Computers were invented to do arithmetic, and Python makes calculating easy. In this chapter you will learn the arithmetic operators, how Python decides the order of operations, and how the math module adds extra power.
The two number types
Python has int for whole numbers and float for numbers with a decimal point. Mixing them in a calculation produces a float:
Arithmetic operators
Here are the operators you will use most often.
| Operator | Name | Example | Result |
|---|---|---|---|
+ | Addition | 7 + 3 | 10 |
- | Subtraction | 7 - 3 | 4 |
* | Multiplication | 7 * 3 | 21 |
/ | Division (always float) | 7 / 2 | 3.5 |
// | Floor division | 7 // 2 | 3 |
% | Modulo (remainder) | 7 % 2 | 1 |
** | Exponent (power) | 2 ** 3 | 8 |
Floor division and modulo
Regular division with / always gives a float. Floor division // throws away the fractional part and keeps the whole number. Modulo % gives the remainder. These two are a great team:
The modulo operator is also the usual way to check whether a number is even: n % 2 == 0 is true for even numbers because they divide by 2 with no remainder.
Exponents
Use ** to raise a number to a power. It also works with fractional powers to find roots:
Operator precedence
When an expression has several operators, Python follows the same precedence rules you learned in school, often remembered as PEMDAS: Parentheses first, then Exponents, then Multiplication and Division (left to right), then Addition and Subtraction (left to right).
In the first line, 3 * 4 happens before the addition, giving 14. Parentheses let you override the default order, as the second line shows. When in doubt, add parentheses to make your intent clear, even if they are not strictly required.
The math module
For more advanced operations, Python includes a math module. You bring it in with an import statement, then use its functions and constants with a dot:
Notice that round() is a built-in function (no import needed) that rounds to a chosen number of decimal places, while math.floor and math.ceil always round down and up to whole numbers. You will learn much more about modules later.
Try a small calculation of your own: compute the area of a circle using math.pi * radius ** 2. Notice how precedence handles the exponent before the multiplication automatically.
4. Strings in Python
Strings in Python
A string is a sequence of characters: letters, digits, spaces, and symbols. Strings represent all the text your programs handle, from names to whole documents. This chapter covers how to create strings, pick them apart, and transform them.
Creating strings
You write a string inside matching single or double quotes. Both are equal; choose whichever avoids clashing with quotes inside the text. For text spanning several lines, use triple quotes:
Indexing and slicing
Each character has a position called an index, starting at 0 for the first character. Negative indexes count from the end, where -1 is the last character. A slice [start:stop] returns the characters from start up to but not including stop.
f-strings
The cleanest way to build a string that includes variable values is the f-string. Put the letter f before the opening quote and write expressions inside curly braces:
Common string methods
Strings come with many built-in methods, called with a dot. Here are the ones you will use most.
| Method | What it does | Example | Result |
|---|---|---|---|
.upper() | All uppercase | "hi".upper() | "HI" |
.lower() | All lowercase | "HI".lower() | "hi" |
.strip() | Remove surrounding spaces | " hi ".strip() | "hi" |
.split() | Break into a list | "a b c".split() | ['a','b','c'] |
.replace() | Swap text | "cat".replace("c","h") | "hat" |
.find() | Index of first match (-1 if none) | "hello".find("l") | 2 |
Immutability
Strings are immutable: once created, they cannot be changed in place. Every method that seems to "change" a string actually returns a brand-new string, leaving the original untouched:
The first s.upper() created "CAT" but we did not store it, so s was unchanged. Only when we reassign s does the variable point to the new value. Remember this pattern: use the return value.
5. Input & Output & Formatting
Input & Output & Formatting
Programs become interactive when they can talk to the user. In this chapter you will read what someone types, convert it to the right type, and control exactly how your output looks.
Reading input with input()
The input() function pauses your program, shows an optional prompt, waits for the user to type something and press Enter, then hands back what they typed as a string:
Converting input to numbers
Because input() always returns a string, you must convert it before doing arithmetic. Use int() for whole numbers and float() for decimals. Forgetting this is one of the most common beginner mistakes:
If we had skipped int(), then age + 10 would have tried to add the number 10 to a string and raised a TypeError. You can also combine the two calls on one line: age = int(input("Your age? ")).
Controlling print() with sep and end
The print() function can take several values separated by commas. By default it joins them with a space and adds a newline at the end. Two keyword arguments change that behaviour:
sepsets what goes between the values.endsets what goes after the last value instead of a newline.
Because the second print ended with "..." instead of a newline, the word "done" appeared on the same line.
Formatting numbers with f-strings
f-strings can also format values. After the expression, add a colon and a format specification. This is invaluable for money, percentages, and tidy tables:
Here .2f means "a float with two decimal places," .1% means "a percentage with one decimal place" (it multiplies by 100 and adds the sign), and the number 5 sets a minimum width so values line up in columns. The < aligns to the left.
A small interactive program
Putting it together, here is a program that asks for two numbers and reports their sum neatly:
You now have the tools to build friendly, interactive programs. Always remember the golden rule of input: convert the string to the number type you need before calculating with it.
6. Conditionals
Conditionals
Real programs must make decisions: show a discount only to members, warn when a battery is low, mark an answer right or wrong. Python makes decisions with the if statement, which runs a block of code only when a condition is true.
The basic if statement
Write the keyword if, a condition, and a colon. The code that runs when the condition is true is indented underneath. Indentation (usually four spaces) is how Python knows which lines belong to the block; it is not optional:
else and elif
Add else for what to do when the condition is false, and elif (short for "else if") to test further conditions in order. Python checks each condition from top to bottom and runs the first block whose condition is true, then skips the rest:
Comparison and logical operators
Conditions are built from comparisons that produce True or False. The comparison operators are == (equal), != (not equal), <, >, <=, and >=. Note the double equals for comparison; a single = is assignment. Combine conditions with the logical operators and, or, and not:
and is true only when both sides are true; or is true when at least one side is true; not flips a value.
Truthiness
Every value in Python can be treated as true or false in a condition. Zero, empty strings, empty lists, and None are "falsy"; almost everything else is "truthy". This lets you write natural checks:
Nesting
An if can live inside another if. Increase the indentation for each level, but avoid nesting too deeply because it quickly becomes hard to read:
Conditionals give your programs the power of choice. Practice by writing a program that reads a number and prints whether it is negative, zero, or positive using if, elif, and else.
7. Loops
Loops
Computers excel at repetition. A loop runs a block of code many times without you writing it out again and again. Python has two loops: the for loop, which repeats once for each item in a sequence, and the while loop, which repeats as long as a condition stays true.
The for loop and range()
A for loop walks through the items of a sequence, giving you one item per pass. The range() function generates a sequence of numbers, which is the usual way to repeat a fixed number of times. range(5) produces 0, 1, 2, 3, 4 (it stops before 5):
range can take a start, stop, and step: range(2, 11, 2) gives 2, 4, 6, 8, 10.
Iterating over sequences
You can loop directly over the characters of a string or, as you will see later, the items of a list:
The while loop
A while loop keeps going as long as its condition is true. You must make sure something inside the loop eventually makes the condition false, or the loop will run forever:
break and continue
Two keywords give you finer control. break leaves the loop immediately. continue skips the rest of the current pass and jumps to the next one:
Odd numbers 1 and 3 print. Even numbers 2 and 4 are skipped by continue, and when n reaches 5 the break stops the loop entirely.
The loop else clause
Both loops may have an else block that runs only if the loop finished normally without hitting a break. It is handy for search loops:
Choosing between for and while
| Use a for loop when... | Use a while loop when... |
|---|---|
| You know how many times to repeat | You repeat until a condition changes |
| You are walking through a sequence | The number of repeats is unknown in advance |
| Example: process each student | Example: keep asking until input is valid |
Loops turn a few lines of code into enormous amounts of work. Practice by printing a multiplication table for a number using a for loop and range.
8. Lists & Tuples
Lists & Tuples
So far each variable has held a single value. Often you need to hold many values together: a shopping list, exam scores, a playlist. Python's list is the workhorse for ordered collections, and the tuple is its unchangeable cousin.
Creating and reading a list
Write a list with square brackets and commas. Items can be any type, and a list keeps them in order. Access items by index, exactly like string characters, and slice them the same way:
Lists are mutable
Unlike strings, lists can be changed in place. You can replace an item by index and use methods to grow or shrink the list.
| Method | What it does | Example on [1, 2] |
|---|---|---|
.append(x) | Add x to the end | [1, 2, 3] |
.insert(i, x) | Insert x at index i | .insert(0, 9) gives [9, 1, 2] |
.pop() | Remove and return last item | returns 2, list is [1] |
.remove(x) | Remove first matching value | .remove(1) gives [2] |
.sort() | Sort the list in place | [2, 1].sort() gives [1, 2] |
Tuples and immutability
A tuple looks like a list but uses round brackets and cannot be changed after creation. Use a tuple when the collection should stay fixed, such as a coordinate pair or a date:
Because tuples cannot change, Python can handle them slightly faster and they are safe to use as fixed records. You can also unpack a tuple into several variables at once: x, y = point gives x = 3 and y = 4.
A gentle look at list comprehensions
A very common task is building a new list by transforming each item of an old one. The long way uses a loop:
Python offers a shorter form called a list comprehension that does the same thing in one line:
Read it as "collect n * n for each n in the range." You can add an if at the end to keep only some items. Comprehensions are concise and popular, but a normal loop is just as correct when it reads more clearly.
Lists and tuples let you manage collections of data with ease. Practice by making a list of your five favourite numbers, then printing their total with the built-in sum() function.
9. Dictionaries & Sets
Dictionaries & Sets
Lists store items by position. Sometimes you would rather look things up by a meaningful label, like a phone book that maps names to numbers. Python's dictionary does exactly that, and the set stores a collection of unique items.
Creating a dictionary
A dictionary holds key-value pairs. Write it with curly braces, each key followed by a colon and its value. Keys are usually strings or numbers and must be unique; values can be anything:
You access a value by putting its key in square brackets. Asking for a key that does not exist raises a KeyError; to avoid that, use .get(), which returns None (or a default you supply) when the key is missing.
Adding, updating, and deleting
Dictionaries are mutable. Assigning to a new key adds it; assigning to an existing key updates it; del removes a pair:
Common dictionary operations
| Operation | Meaning | Example |
|---|---|---|
d[key] | Read a value | student["name"] |
d[key] = v | Add or update | student["grade"] = "A" |
del d[key] | Delete a pair | del student["age"] |
key in d | Test membership | "name" in student |
d.get(key, default) | Safe read | student.get("x", 0) |
d.keys() / d.values() | All keys / values | student.keys() |
Iterating over a dictionary
Looping over a dictionary gives you the keys. The .items() method is the usual way to get both the key and value each pass:
Sets and uniqueness
A set is an unordered collection that automatically removes duplicates. Create one with curly braces (but with no colons) or the set() function. Sets are great for membership tests and for removing repeats from a list:
Note that a set has no indexes because it has no order, so you cannot write unique[0].
Set operations
Sets support the mathematical operations of union, intersection, and difference. These answer questions like "what do two groups share?" quickly and clearly:
The | operator gives the union (everything in either set), & gives the intersection (items in both), and - gives the difference (items in the first but not the second).
Dictionaries and sets round out Python's core data structures. Reach for a dictionary whenever you want to look data up by name, and for a set whenever uniqueness or fast membership checks matter. Practice by counting how many times each letter appears in a word using a dictionary.
10. Functions
Functions
As programs grow, repeating the same lines in many places becomes error-prone. A function is a named, reusable block of code. You define it once and call it whenever you need it, often with different inputs. Functions make programs shorter, clearer, and easier to fix.
Defining and calling a function
Use the keyword def, a name, parentheses for any inputs, and a colon. The body is indented. Call the function by writing its name followed by parentheses:
Parameters and return values
The names in the parentheses are parameters; the values you pass in when calling are arguments. The return statement sends a value back to whoever called the function, so you can use the result:
A function without a return statement automatically returns None. The difference between print and return matters: print only shows text on screen, while return gives a value your program can keep using.
Default and keyword arguments
A parameter can have a default value, used when the caller does not supply one. You may also pass arguments by name, which makes a call self-explanatory and lets you skip earlier optional values:
Scope
Variables created inside a function are local: they exist only while the function runs and cannot be seen from outside. This keeps functions independent and prevents them from accidentally interfering with each other:
Docstrings
A docstring is a string placed as the first line inside a function to describe what it does. Tools and the built-in help() function can read it, and it reminds future readers (including you) of the function's purpose:
A gentle look at *args and **kwargs
Sometimes you want a function to accept any number of arguments. Writing *args collects extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dictionary:
You will not need these every day, but they power many flexible library functions. For now, remember the core idea: a function takes inputs, does work, and returns a result.
Functions are the main way programmers organise ideas. Practice by writing a function that takes a temperature in Celsius and returns it in Fahrenheit using the formula c * 9 / 5 + 32.
11. Modules & the Standard Library
Modules & the Standard Library
No one writes every program from scratch. Python comes with a huge standard library of ready-made code, grouped into modules. A module is simply a file of Python definitions you can bring into your program. This chapter shows how to import modules, tours a few useful ones, and explains how to install extra packages.
Import forms
There are three common ways to import. The first brings in the whole module; you then use its contents with a dot. The second imports specific names directly. The third renames a module to a shorter alias:
Prefer import math over from math import * (which imports everything): keeping the module name makes it obvious where a function came from.
The random module
The random module produces random numbers and choices, perfect for games and simulations. (Your exact numbers will differ each run because they are random.)
randint(1, 6) returns a whole number between 1 and 6 inclusive, like rolling a die. choice picks one item from a list, and shuffle rearranges a list in place.
The datetime module
The datetime module works with dates and times. Here we get today's date and format it nicely:
In a real program you would call date.today() to get the current date automatically. The strftime method turns a date into text using format codes such as %Y for the four-digit year and %B for the month name.
pip and third-party packages
Beyond the standard library, the community has published hundreds of thousands of free packages on the Python Package Index (PyPI). You install them with pip, Python's package installer, from your terminal (not inside a Python script):
Once installed, you import it just like a standard module:
Popular packages include requests for web requests, pandas for data analysis, and pygame for games. Installing packages per project, inside a virtual environment, is a good habit you will learn as you progress.
Splitting your own code into modules
Any .py file you write is itself a module that other files can import. Suppose you save this as tools.py:
Then in another file in the same folder you can use it:
Splitting code into modules keeps each file focused and lets you reuse your work across projects. As your programs grow, organising them into several small modules is far easier to manage than one enormous file.
The standard library is sometimes called "batteries included" because so much is ready to use. Whenever you face a common task, search the documentation first; the tool you need may already exist.
12. File Handling
File Handling
Programs often need to remember information after they stop running, or to read data someone else prepared. Files on disk make this possible. Python can create, read, and write text files with just a few lines of code.
Opening a file
The open() function connects your program to a file. You give it a file name and a mode that says what you intend to do. The mode is a short string.
| Mode | Meaning | If file exists |
|---|---|---|
"r" | Read (the default) | Reads it; error if missing |
"w" | Write | Erases and overwrites it |
"a" | Append | Adds to the end |
"x" | Create new | Error if it already exists |
The with statement
The recommended way to work with a file is the with statement. It opens the file, gives you a variable to use inside the indented block, and automatically closes the file afterwards, even if an error occurs. Forgetting to close files is a classic bug that with prevents:
The \n is a newline character; without it, both pieces of text would run together on one line.
Reading a whole file
To read the file back, open it in read mode. The .read() method returns the entire contents as one string:
Reading line by line
For larger files it is better to process one line at a time. You can loop directly over the file object, which yields each line in turn, or use .readlines() to get a list of all lines:
We call .strip() on each line to remove the trailing newline so print does not add a second blank line. .readlines() would instead give ['First line\n', 'Second line\n'].
Appending to a file
Open in append mode to add to a file without erasing what is already there:
A note about CSV files
A very common file format is CSV (comma-separated values), used by spreadsheets. Each line is a row and commas separate the columns. You could split lines yourself, but Python's built-in csv module handles the tricky details for you:
Notice that every value read from a CSV file comes back as a string, so you would convert row[1] with int() if you needed the age as a number.
With files, your programs can save results, keep logs, and read data prepared elsewhere. Always prefer the with statement so your files are closed safely, and be careful with "w" mode because it erases existing content.
13. Exceptions & Error Handling
Exceptions & Error Handling
Things go wrong: a file is missing, a user types "cat" where a number was expected, a calculation divides by zero. When Python hits such a problem it raises an exception, and unless you handle it, your program stops with an error message. This chapter shows how to catch exceptions and respond gracefully.
Why handle errors?
An unhandled exception crashes your program and shows a scary traceback to the user. Handling errors lets your program recover, ask the user to try again, or save their work. Consider this crash:
try and except
Wrap risky code in a try block. If an exception occurs, Python jumps to the matching except block instead of crashing:
It is best to catch the specific exception you expect, like ValueError, rather than catching everything. That way genuine, unexpected bugs still surface instead of being silently swallowed.
Common exception types
| Exception | Happens when... | Example |
|---|---|---|
ValueError | Right type, wrong value | int("abc") |
TypeError | Wrong type used | "a" + 1 |
ZeroDivisionError | Dividing by zero | 5 / 0 |
IndexError | List index out of range | [1, 2][5] |
KeyError | Missing dictionary key | {}["x"] |
FileNotFoundError | Opening a missing file | open("no.txt") |
else and finally
A full error-handling statement can have four parts. The else block runs only if no exception occurred. The finally block always runs, whether or not there was an error, which is ideal for cleanup:
Catching more than one exception
You can handle several exception types, each with its own response, or group them:
Raising your own exceptions
Sometimes your own code discovers a problem and should signal it. Use the raise keyword to raise an exception with a helpful message. This lets callers handle the problem the same way they handle built-in errors:
Writing except ValueError as e captures the exception object in e, so you can print its message. A common real-world use is validating user input: ask again until the value makes sense.
Good error handling is the difference between a fragile program and a robust one. Catch the specific errors you expect, give the user a clear message, and let unexpected bugs surface so you can fix them.
14. Classes & Objects (OOP)
Classes & Objects (OOP)
So far we have organised code with functions. Object-oriented programming (OOP) offers another powerful way: bundle related data and the functions that act on it into a single unit called an object. A class is the blueprint; an object (or instance) is a specific thing built from that blueprint.
Defining a class
Use the keyword class and a capitalised name by convention. The special method __init__ runs automatically when you create a new object; it sets up the object's starting data. The first parameter, always called self, refers to the particular object being worked on:
The variables self.name and self.age are attributes: data belonging to each object. bark is a method: a function that belongs to the class and can use the object's attributes through self.
Many objects from one class
From a single class you can create as many independent objects as you like. Each carries its own attribute values:
A worked example: a bank account
Classes shine when data and behaviour belong together. A bank account has a balance (data) and operations like deposit and withdraw (behaviour) that must keep the balance sensible:
The account started at 100, gained 50, lost 30, and refused the oversized withdrawal, ending at 120. Notice how the withdraw method protects the data by checking before subtracting. Keeping such rules inside the class is one of OOP's main benefits.
A brief look at inheritance
Inheritance lets a new class build on an existing one, reusing its code and adding or changing behaviour. The new class lists the parent in parentheses:
Cat inherits the __init__ method from Animal, so it already knows how to store a name, but it provides its own speak method, replacing the parent's version. This ability to reuse and specialise code keeps large programs manageable.
Object-oriented programming takes practice to appreciate fully. For now, remember the core idea: a class groups related data (attributes) and actions (methods) into one tidy package, and you create as many objects from it as you need.
The four pillars of OOP
Once you can define a class and create objects, the next step is understanding why OOP is organised the way it is. Almost everything you will meet from here on is an expression of four core ideas, usually called the four pillars of object-oriented programming. You do not need to memorise the names, but recognising them helps you see the pattern behind the syntax.
| Pillar | One-line meaning | Everyday analogy |
|---|---|---|
| Encapsulation | Bundle data with the code that works on it, and control who can touch it. | A medicine capsule: the contents are sealed inside; you take the pill, you don't repackage the powder. |
| Inheritance | A new class can reuse and extend an existing one ("is-a"). | A sports car is a car: it has everything a car has, plus extras. |
| Polymorphism | The same method name behaves correctly for different types. | Pressing "play" works on a phone, a TV and a car stereo, even though each does it differently. |
| Abstraction | Expose a simple interface and hide the messy details. | Driving a car: you use the steering wheel and pedals without knowing how the engine burns fuel. |
The rest of this chapter walks through each pillar with runnable code. Keep the table in mind: every feature below is a tool for one of these four goals.
Instance attributes vs class attributes
There are two places data can live in a class, and mixing them up is one of the most common beginner bugs.
- Instance attributes are created inside
__init__usingself. Each object gets its own private copy. - Class attributes are defined directly in the class body, outside any method. There is only one copy, shared by every instance of the class.
Notice that changing Dog.species affected both existing dogs instantly, because they all read the same shared value. Instance attributes like name stay independent. Class attributes are perfect for constants and defaults that genuinely belong to the whole type.
The mutable class-attribute trap. Problems appear when a shared class attribute is something changeable, like a list. Watch what happens:
Rex somehow knows Bella's trick! Because tricks is one shared list, every dog appends to the same object. The fix is to make the list an instance attribute, created fresh for each dog inside __init__:
Rule of thumb: use class attributes for constants shared by every object; put anything each object should own separately (especially mutable things like lists and dicts) in __init__ via self.
Three kinds of methods: instance, class, and static
So far every method you wrote took self. That is just one of three method types Python offers, each with a different job.
- Instance method — takes
self; works with one specific object's data. This is the default. - Class method — marked with
@classmethod, takescls(the class itself). Great as an alternative constructor / factory. - Static method — marked with
@staticmethod, takes neitherselfnorcls. It is just a plain utility function that logically belongs inside the class.
Why use cls instead of writing Pizza(...) directly in the factory? Because cls respects inheritance: if a subclass calls margherita(), it builds an instance of the subclass, not of Pizza. The static area method does not read any object state, so it takes neither self nor cls and can be called straight off the class.
Encapsulation: controlling access
Python does not have true "private" keywords like some languages. Instead it uses naming conventions plus one mild technical trick:
name— public: part of the official interface, use freely._name— protected (single underscore): "internal, please don't touch from outside." A convention only; Python does not stop you.__name— private (double underscore): Python performs name mangling, rewriting it to_ClassName__nameso it is hard to hit by accident.
So __pin is not truly hidden; it has merely been renamed to _Account__pin. The point of the double underscore is to avoid accidental clashes and signal "really internal," not to provide security.
The clean way to guard data: @property. A property lets an attribute look like a plain variable from the outside while secretly running a getter and setter. This is how you validate data without forcing callers to use ugly get_x() / set_x() methods.
From the caller's point of view, t.celsius = 100 is just an assignment. Under the hood the setter runs, rejecting impossible values. fahrenheit has only a getter, so it is effectively read-only and always computed from the current Celsius. This is encapsulation done the Pythonic way: start with plain public attributes, and reach for @property only when you need validation or a computed value.
Inheritance in depth
Inheritance models an is-a relationship: a Dog is an Animal. The child class automatically gets the parent's attributes and methods, and can add new ones or replace (override) existing ones. Use super().__init__() to run the parent's constructor so you don't rewrite its setup.
The arrows point from the specific class up to the general one: "Dog is-a Animal." Here is a three-level hierarchy where each level overrides speak():
Look closely at describe(): it is defined only on Animal, yet it prints the right sound for each animal. That is because it calls self.speak(), and self looks up speak on the actual object's class first. The Puppy never defines its own __init__, so it transparently uses Dog's, which is why p.breed works.
Two built-in functions let you ask about relationships at runtime:
Multiple inheritance and the MRO (briefly). Python allows a class to inherit from more than one parent at once, e.g. class D(B, C). When you call a method, Python searches the parents in a fixed order called the Method Resolution Order (MRO), which you can inspect with ClassName.__mro__. It generally goes left-to-right, depth-first, with each class appearing once:
Multiple inheritance is powerful but easy to misuse; for most programs, prefer single inheritance (or composition, covered later) and keep hierarchies shallow.
Polymorphism and duck typing
Polymorphism ("many forms") means the same method name can be called on different types and each does the right thing. Because of this, you can loop over a mixed collection of objects and call one method without caring which exact class each one is:
Notice these three classes do not share a parent. Python does not check their type; it only checks that each object has a speak() method. This is called duck typing: "if it walks like a duck and quacks like a duck, treat it like a duck." What matters is the behaviour an object offers, not its label.
The Robot has nothing to do with animals, yet make_it_speak accepts it happily. Duck typing keeps Python code flexible: you program to a capability, not to a rigid type hierarchy.
Abstraction with abstract base classes
Sometimes you want to guarantee that every subclass implements a particular method, and you want to forbid creating the vague "base" type on its own. The abc module (Abstract Base Classes) provides exactly this. You inherit from ABC and mark required methods with @abstractmethod.
Shape describes what every shape must be able to do (compute an area) without saying how. Trying to create a bare Shape() raises TypeError, because it has an unimplemented abstract method. If you wrote a Triangle(Shape) and forgot to define area, Python would refuse to instantiate it too. This is abstraction enforced by the language: the base class defines the contract, concrete subclasses fulfil it.
Special (dunder) methods
Methods with double underscores on both sides (__init__, __str__, ...) are called dunder ("double underscore") or magic methods. Python calls them automatically in response to built-in operations like print(), ==, len(), + and sorting. Defining them makes your objects feel like native Python types.
By default, printing an object is unhelpful:
(The exact hexadecimal address varies every run.) Two dunders fix this. __str__ gives a friendly, human-readable form used by print() and str(); __repr__ gives an unambiguous, developer-facing form used in the interactive shell, in containers, and as a fallback when __str__ is missing.
A good habit: always define at least __repr__. If you only define one, make it __repr__, because __str__ falls back to it.
Value equality with __eq__. By default == compares identity (are these the same object in memory?). Define __eq__ to compare by value instead:
Without __eq__, the first line would print False, because the two points are distinct objects even though their values match.
Making objects sortable with __lt__. The sorted() function and < operator use __lt__ ("less than"). Define it and your objects sort automatically:
Length with __len__, and operators with __add__. __len__ lets len() work on your object; __add__ lets + combine two objects:
| Dunder method | What it enables |
|---|---|
__init__ | Set up a new object (the constructor). |
__str__ | Friendly text for print() / str(). |
__repr__ | Unambiguous text for debugging / containers. |
__eq__ | Compare two objects with == by value. |
__lt__ | Order objects with < and sorted(). |
__len__ | Make len(obj) work. |
__add__ | Define what obj1 + obj2 does. |
__getitem__ | Enable indexing, e.g. obj[0]. |
Composition vs inheritance
Inheritance models is-a ("a Dog is an Animal"). Composition models has-a ("a Car has an Engine"): instead of inheriting, one object simply holds another object as an attribute and delegates work to it.
A Car is clearly not a kind of Engine, so inheritance would be wrong here; the car simply contains one. Composition is more flexible: you could swap in an ElectricEngine, or give the car several components (wheels, radio, battery) without tangling them into one inheritance chain.
Rule of thumb: if you can say "X is a Y," consider inheritance; if you can say "X has a Y," prefer composition. When unsure, favour composition — it leads to looser coupling and fewer surprises.
Best practices & common pitfalls
- One responsibility per class. A class should model one clear thing. If it is doing networking and maths and file I/O, split it up.
- Prefer composition for "has-a". Reach for inheritance only when there is a genuine is-a relationship, and keep hierarchies shallow.
- Don't overuse inheritance. Deep chains and multiple inheritance make code hard to follow. Most designs need only one or two levels.
- Use
@propertyfor validated access. Expose plain attributes by default; switch to a property when you need to validate input or compute a value, rather than littering code withget_x()/set_x(). - Never forget
self. Every instance method's first parameter isself, and you access object data through it (self.name, not justname). - Avoid mutable defaults and mutable class attributes. Create lists/dicts inside
__init__so each object owns its own, instead of sharing one by accident. - Define
__repr__(and often__str__). Readable objects make debugging and logging far easier. - Keep methods short and named for what they do.
account.withdraw(50)reads better than one giantdo_everything()method.
Quick reference
| Feature | Syntax | Purpose |
|---|---|---|
| Define a class | class Dog: | Create a new type. |
| Constructor | def __init__(self, ...): | Set up a new object's data. |
| Current object | self | Refers to the instance inside its methods. |
| Class attribute | species = "..." (in class body) | Value shared by all instances. |
| Class method | @classmethod / def m(cls): | Factory or alternative constructor. |
| Static method | @staticmethod / def m(): | Utility that needs no object state. |
| Validated attribute | @property + @x.setter | Getter/setter that looks like an attribute. |
| Inheritance | class Dog(Animal): + super().__init__() | Reuse and extend a parent class. |
| Abstract method | @abstractmethod (class inherits ABC) | Force subclasses to implement a method. |
| Readable text | __str__ / __repr__ | Control how the object prints. |
| Value equality | __eq__ | Compare objects with ==. |
15. Putting It Together
Putting It Together
You have learned variables, operators, strings, conditionals, loops, data structures, functions, modules, files, error handling, and classes. That is everything you need to build real programs. In this final chapter you will combine these ideas into three small projects, then plan how to keep growing as a programmer.
Project 1: Number-guessing game
The computer picks a secret number and you guess until you find it. This project uses the random module, a while loop, conditionals, and input conversion with error handling:
The while True loop runs forever until break stops it on a correct guess. The try/except keeps a bad entry from crashing the game.
Project 2: Temperature converter
This project turns the conversion formulas into reusable functions, then offers the user a simple menu. It practises functions, f-strings, and clean output formatting:
Because we called .upper() on the choice, the user can type in any mix of upper and lower case. Each formula lives in its own function, so you could reuse them in another program.
Project 3: Simple to-do list
This project stores tasks in a list and lets the user add, view, and remove them in a loop. It brings together lists, a menu-driven while loop, and functions:
The built-in enumerate function gives both a counter and each item, which is perfect for numbered lists. As an extension, you could save the tasks to a file so they survive after the program closes, using the file-handling skills from an earlier chapter.
How to keep learning
You now have a solid foundation. To grow further:
- Build things. Pick small projects you find useful: a tip calculator, a password generator, a quiz. Building cements knowledge far better than reading alone.
- Read the official docs. The Python documentation and its tutorial at docs.python.org are excellent and free.
- Learn to debug. Read error messages carefully, print values to see what your program is really doing, and change one thing at a time.
- Explore libraries. When ready, try
pandasfor data,flaskfor web apps, orpygamefor games. - Practice regularly. A little coding most days beats an occasional marathon. Sites with small exercises are great for keeping sharp.
Congratulations on reaching the end of this course. Programming is a craft you improve for a lifetime, and you have taken the most important step: starting. Keep writing code, stay curious, and enjoy the power to turn your ideas into working programs.