IntermediateLesson 40 of 4714 min read12 runnable examples
A regular expression (regex) is a small pattern language for finding and extracting text: every email address in a page, every number in a log, every date in a report. Python’s re module runs these patterns for you.
In this lesson
search, match, fullmatch, findall and sub
Character classes and quantifiers
Capture groups and named groups
Flags and compiled patterns
Your first regex
Write patterns as raw strings (r"...") so backslashes reach the regex engine unchanged.
Python
importretext="Order 4521 shipped on 2026-11-02, order 4522 pending"match=re.search(r"\d+",text)# first run of digitsprint(match.group(),match.start(),match.end())print(re.findall(r"\d+",text))# every run of digits
Output
4521 6 10
['4521', '2026', '11', '02', '4522']
The main functions
Function
Returns
re.search(p, s)
First match anywhere, or None
re.match(p, s)
Match only at the start, or None
re.fullmatch(p, s)
Match only if the whole string fits
re.findall(p, s)
List of all matches (or of groups)
re.finditer(p, s)
Iterator of match objects
re.sub(p, repl, s)
String with matches replaced
re.split(p, s)
List split on the pattern
Python
importreprint(re.match(r"\d+","123abc"))print(re.match(r"\d+","abc123"))print(bool(re.fullmatch(r"\d{6}","411001")),bool(re.fullmatch(r"\d{6}","41100")))print(re.sub(r"\s+"," ","too many spaces"))print(re.split(r"[,;]\s*","a, b;c ,d"))
Output
<re.Match object; span=(0, 3), match='123'>
None
True False
too many spaces
['a', 'b', 'c ', 'd']
Tip
Always check for None before calling .group() on the result of search or match: if m := re.search(p, s): print(m.group()).
importreprint(re.sub(r"(\d{4})-(\d{2})-(\d{2})",r"\3/\2/\1","Due 2026-11-02 and 2026-12-25"))
Output
Due 02/11/2026 and 25/12/2026
Flags
Python
importretext="Python\npython\nPYTHON"print(re.findall(r"python",text,flags=re.IGNORECASE))print(re.findall(r"^p\w+",text,flags=re.I|re.M))# ^ at every line start
\d matches one digit at a time, \d+ matches each run of digits, and sub replaces each run with #.
Quick quiz
1. What is the difference between re.match and re.search?
Use search unless you specifically need the start.
2. What does \d{3} match?
{3} repeats the previous item exactly three times.
3. Why write patterns as raw strings like r"\d+"?
Without r, "\b" would be a backspace character instead of a word boundary.
4. What does re.findall return when the pattern has one group?
With groups, findall returns the captured parts.
Exercise: extract phone numbers
Find all 10-digit Indian mobile numbers (starting with 6, 7, 8 or 9) in the text, allowing an optional +91 and a space or hyphen after the first 5 digits.
Input
"Call 98765-43210 or +91 91234 56789; old: 12345 67890"
The pattern requires the first digit to be 6-9, so “12345 67890” is skipped. re.sub(r"\D", "", ...) removes everything that is not a digit, and [-10:] drops the 91 prefix.
Program Practice
20 Python RegEx Programs
Every program has a step-by-step explanation, colour-coded code and its real output.
re is the standard-library module for regular expressions. It provides search, match, findall, sub, split and compile.
How do I extract numbers from a string in Python?
re.findall(r"\d+", text) returns every run of digits as strings. Use r"-?\d+(?:\.\d+)?" to include negatives and decimals.
What does re.compile do?
It turns a pattern into a reusable object. It makes code clearer when the same pattern is used many times; Python also caches recently used patterns automatically.
How do I make a regex case-insensitive?
Pass flags=re.IGNORECASE (or re.I), or start the pattern with (?i).
IntermediateLesson 35 of 4712 min read6 runnable examples
A module is simply a .py file whose code you can reuse with import. Python ships with hundreds of modules (the standard library), and pip installs thousands more from PyPI. This lesson shows how to use both, and how to organise your own code into modules.
In this lesson
The different import styles
Create and import your own module
What if __name__ == “__main__” means
Install packages with pip in a virtual environment
When a file is run directly, its __name__ is "__main__". When it is imported, __name__ is the module’s name. This lets a file be both a reusable module and a runnable script:
greeter.py
defgreet(name):returnf"Hello, {name}"print("__name__ is",__name__)if__name__=="__main__":print(greet("from the command line"))
Output
__name__ is __main__
Hello, from the command line
app.py
importgreeterprint(greeter.greet("from app.py"))
Output
__name__ is greeter
Hello, from app.py
When app.py imports greeter, the top-level print runs with __name__ set to greeter, but the code under the if does not.
Packages
A package is a folder of modules. Import modules from it with dots:
Terminal
myshop/ __init__.py pricing.py cart.pymain.py -> from myshop.pricing import with_tax
The standard library
Module
Use it for
os, pathlib, shutil
Files, folders and paths
sys
Interpreter details, command-line arguments
datetime, time
Dates, times and timers
json, csv
Reading and writing data files
re
Regular expressions
collections, itertools
Extra containers and iterator tools
random, math, statistics
Numbers
logging
Log messages
unittest
Testing (pytest is the popular third-party choice)
Install packages with pip
pip downloads packages from PyPI. Always run it through the Python you are using: python -m pip install requests.
Terminal
$ python-mpip--versionpip 26.2.2 from C:\Python3\Lib\site-packages\pip (python 3.15)
Use a virtual environment
A virtual environment is a private folder of packages for one project, so different projects can use different versions. Create one and check that its Python is used:
IntermediateLesson 33 of 4711 min read6 runnable examples
Polymorphism means “many forms”: different objects respond to the same method call in their own way. Encapsulation means keeping an object’s data behind its methods so it cannot be put into an invalid state. Together with inheritance they are the core ideas of object-oriented programming.
In this lesson
Polymorphism with overriding and duck typing
Operator overloading with special methods
Private attributes and name mangling
Getters and setters with @property
Polymorphism through overriding
Each class implements the same method name. Code that calls the method does not need to know which class it is dealing with.
Python does not require a shared parent class. If an object has the method you call, it works. “If it walks like a duck and quacks like a duck, it is a duck.”
Python has no true private attributes, but it has two conventions:
_name (one underscore): “internal, please do not touch from outside”.
__name (two underscores): Python renames it to _ClassName__name (name mangling), so it is hard to reach by accident.
Python
classWallet:def__init__(self):self._currency="INR"# internal by conventionself.__balance=0# name-mangleddefadd(self,amount):self.__balance+=amountdefbalance(self):returnself.__balancew=Wallet()w.add(500)print(w.balance(),w._currency)print(w._Wallet__balance)# still reachable if you insistprint(w.__balance)
Output
500 INR
500
Traceback (most recent call last):
File "main.py", line 16, in <module>
print(w.__balance)
^^^^^^^^^^^
AttributeError: 'Wallet' object has no attribute '__balance'. Did you mean '.balance' instead of '.__balance'?
Getters and setters with @property
@property lets you validate values while keeping simple attribute syntax for the caller:
Python
classProduct:def__init__(self,name,price):self.name=nameself.price=price# goes through the setter below@propertydefprice(self):returnself._price@price.setterdefprice(self,value):ifvalue<0:raiseValueError("price cannot be negative")self._price=value@propertydefprice_with_tax(self):# read-only computed attributereturnround(self._price*1.18,2)p=Product("Mouse",349)p.price=399print(p.price,p.price_with_tax)try:p.price=-5exceptValueErrorase:print("Error:",e)
Output
399 470.82
Error: price cannot be negative
Tip
Start with plain public attributes. Switch to @property only when you need validation or a computed value; callers do not need to change their code.
Cat and Robot share no parent class, but both have speak(), so duck typing lets the loop call it on each.
Quick quiz
1. What does duck typing mean?
Python cares about behaviour, not declared types.
2. What does __add__ enable?
a + b calls a.__add__(b).
3. What happens to self.__secret inside class Box?
Name mangling makes accidental access harder but not impossible.
Exercise: temperature with validation
Create a Thermostat with a target property that only accepts values from 16 to 30 and raises ValueError otherwise.
Input
set target to 24, then try 35
Expected output
24
Rejected: 35 is outside 16-30
24
Hint 1
Store the real value in self._target inside the setter.
Python
classThermostat:def__init__(self,target=22):self.target=target@propertydeftarget(self):returnself._target@target.setterdeftarget(self,value):ifnot16<=value<=30:raiseValueError(f"{value} is outside 16-30")self._target=valuet=Thermostat()t.target=24print(t.target)try:t.target=35exceptValueErrorase:print("Rejected:",e)print(t.target)
Output
24
Rejected: 35 is outside 16-30
24
Every assignment to target, including the one in __init__, goes through the setter, so an invalid value can never be stored. The failed assignment leaves the old value unchanged.
Program Practice
48 Python OOP Programs
Every program has a step-by-step explanation, colour-coded code and its real output.
It is the ability of different objects to respond to the same call. len("abc") and len([1, 2]) both work, and shape.area() returns the right result for a Circle or a Square.
Does Python have private variables?
Not strictly. A leading underscore marks an attribute as internal, and a double underscore triggers name mangling. Both are conventions that discourage outside access.
What is @property in Python?
A decorator that turns a method into an attribute-like getter. Add @name.setter to run validation when the attribute is assigned.
Does Python support method overloading?
Not by signature. Define one method with default or variable arguments (*args) instead. Operator overloading is supported through special methods such as __add__.
IntermediateLesson 32 of 4711 min read8 runnable examples
Inheritance lets a new class reuse and extend an existing one. The child class gets all the parent’s attributes and methods, and can add its own or change how some of them behave. Page Object frameworks use this to share common page code in a BasePage.
In this lesson
Create a child class
Extend __init__ with super()
Override methods
Multiple inheritance and the MRO
A parent and a child
Put the parent class in brackets after the child’s name. The child inherits everything from the parent.
Python
classVehicle:def__init__(self,brand):self.brand=branddefstart(self):returnf"{self.brand} engine started"classCar(Vehicle):# Car inherits from Vehiclepassc=Car("Tata")print(c.start())print(isinstance(c,Car),isinstance(c,Vehicle))
Output
Tata engine started
True True
Add attributes with super()
To add new attributes, define __init__ in the child and call the parent’s __init__ with super(), so the parent’s setup still happens.
Python
classVehicle:def__init__(self,brand):self.brand=brandclassCar(Vehicle):def__init__(self,brand,seats):super().__init__(brand)# let Vehicle set brandself.seats=seats# then add Car's own datac=Car("Mahindra",7)print(c.brand,c.seats)
Output
Mahindra 7
Watch out
If the child defines __init__ and forgets super().__init__(...), the parent’s attributes are never set and you get AttributeError later.
Override methods
A method with the same name in the child replaces the parent’s version. Call super().method() to reuse the parent’s code inside the new one.
Python
classEmployee:def__init__(self,name,salary):self.name=nameself.salary=salarydefannual_pay(self):returnself.salary*12defdescribe(self):returnf"{self.name} earns {self.annual_pay()} a year"classManager(Employee):def__init__(self,name,salary,bonus):super().__init__(name,salary)self.bonus=bonusdefannual_pay(self):# overridereturnsuper().annual_pay()+self.bonusstaff=[Employee("Asha",50000),Manager("Ravi",80000,100000)]forpersoninstaff:print(person.describe())
Output
Asha earns 600000 a year
Ravi earns 1060000 a year
describe() is defined only in Employee, yet for a Manager it calls the overridden annual_pay(). Python always looks up the method on the actual object.
A real example: page objects
Python
classBasePage:url="/"def__init__(self,base_url):self.base_url=base_urldefopen(self):returnf"GET {self.base_url}{self.url}"classLoginPage(BasePage):url="/login"deflogin(self,user):returnf"{self.open()} -> sign in as {user}"classCartPage(BasePage):url="/cart"print(LoginPage("https://shop.test").login("asha"))print(CartPage("https://shop.test").open())
Output
GET https://shop.test/login -> sign in as asha
GET https://shop.test/cart
A class can inherit from several parents. When two parents define the same method, Python follows the method resolution order (MRO): the child first, then the parents from left to right.
16
Traceback (most recent call last):
File "main.py", line 15, in <module>
Shape()
~~~~~^^
TypeError: Can't instantiate abstract class Shape without an implementation for abstract method 'area'
super() gives access to methods of the parent class (strictly, the next class in the MRO), most often to call the parent’s __init__ or an overridden method.
Does Python support multiple inheritance?
Yes. A class can list several parents. Python resolves name clashes with the method resolution order (C3 linearisation), which you can inspect with ClassName.__mro__.
What is the difference between inheritance and composition?
Inheritance models “is a” (a Manager is an Employee). Composition models “has a” (a Car has an Engine) by storing another object as an attribute. Prefer composition when the relationship is not truly “is a”.
IntermediateLesson 28 of 479 min read8 runnable examples
Every for loop in Python uses an iterator behind the scenes. Knowing how iterators work explains why some objects can be looped over only once, and lets you write your own objects that work with for.
In this lesson
Iterables vs iterators
iter(), next() and StopIteration
Build a custom iterator class
Useful tools from itertools
Iterable vs iterator
An iterable is anything you can loop over: a list, string, tuple, dict, set, file or range.
An iterator is the object that actually hands out the items, one at a time, and remembers its position.
iter() gets an iterator from an iterable, and next() asks it for the next item:
An iterator moves forward only. Once it is exhausted it stays empty. A list can be looped over many times because each for loop asks it for a fresh iterator.
Python
squares=map(lambdax:x*x,[1,2,3])# map returns an iteratorprint(list(squares))print(list(squares))# already used upnums=[1,2,3]print(sum(nums),sum(nums))# a list gives a new iterator each time
Output
[1, 4, 9]
[]
6 6
Write your own iterator
A class becomes an iterator by defining __iter__() (returning itself) and __next__() (returning the next item or raising StopIteration):
What is the difference between an iterable and an iterator?
An iterable can produce an iterator (lists, strings, dicts). An iterator produces the items one at a time with next() and remembers its position. Every iterator is also iterable.
Why can I loop over a list twice but not over map()?
A list creates a new iterator for each loop. map() is itself an iterator, so once its items are consumed it is empty.
How do I make a class iterable in Python?
Define __iter__() returning an iterator. The easiest way is to make __iter__ a generator method using yield.