← Back to Python series
⚙️
Intermediate
class · __init__ · Methods · Properties · dataclass · Inheritance · super() · Polymorphism · __str__ · __eq__ · __len__ · re module · Patterns · Groups · Flags · findall · sub · CLI · File I/O · OOP · Regex · Statistics

Weeks 7–10 — OOP Part 1: Classes & Instances · OOP Part 2: Inheritance & Dunder Methods · Regular Expressions · Intermediate Capstone: Text Analysis Tool

Model real-world entities with classes. Learn to define attributes in __init__, write instance methods, use properties for validation, and simplify data classes with @dataclass. Extend classes with inheritance, call parent methods with super(), implement polymorphism, and customize object behavior with dunder (magic) methods. Unlock powerful text-matching with regular expressions. Learn pattern syntax, groups, lookahead/lookbehind, and use re.findall, re.sub, and re.fullmatch for text extraction and cleaning. Combine the intermediate track skills to build a command-line text analysis tool that reads files, tokenizes text, computes statistics, and produces a formatted report.

OOPclassinstance__init__propertydataclassinheritancesuperpolymorphismdunderABCregexrepatterngroupsfindallsubcapstoneprojectCLItext analysis
Duration
⏱ 3 hours + 3 hours + 2.5 hours + 3 hours
Level
📊 Intermediate
Prerequisite
🎯 Basic Week 9
OUTCOME
Model a bank account with deposit, withdraw, and balance validation / Build a shape hierarchy with area/perimeter polymorphism and rich comparison / Build a log parser that extracts IPs, timestamps, and error messages using regex / A CLI tool that analyses any text file and prints a statistics report

What you'll learn

  • 1Define a class with __init__ and instance attributes
  • 2Write instance methods that operate on self
  • 3Use @property for computed attributes and validation
  • 4Distinguish class attributes from instance attributes
  • 5Create simple data containers with @dataclass
  • 6Create a child class that inherits from a parent
  • 7Call parent __init__ with super()
  • 8Override methods for polymorphism
  • 9Define abstract base classes with ABC
  • 10Implement __str__, __repr__, __eq__, __lt__, __len__, __add__
  • 11Write regex patterns for common scenarios (email, phone, date)
  • 12Use groups () and named groups (?P<name>...)
  • 13Apply re.match, re.search, re.findall, re.sub
  • 14Use regex flags: re.IGNORECASE, re.MULTILINE
  • 15Understand greedy vs non-greedy quantifiers
  • 16Integrate file I/O, regex, OOP, and collections in one project
  • 17Design a class hierarchy for the analysis pipeline
  • 18Handle command-line arguments with argparse
  • 19Produce formatted tabular output
  • 20Write unit tests for the tokenizer and statistics functions

Week 7 — OOP Part 1: Classes & Instances

Model real-world entities with classes. Learn to define attributes in __init__, write instance methods, use properties for validation, and simplify data classes with @dataclass.

1. Classes and Instances

python
class Dog:
    species = "Canis lupus familiaris"  # class attribute

    def __init__(self, name, age):
        self.name = name   # instance attribute
        self.age = age

    def bark(self):
        return f"{self.name} says: Woof!"

    def __repr__(self):
        return f"Dog(name={self.name!r}, age={self.age})"

fido = Dog("Fido", 3)
print(fido.bark())
print(repr(fido))
print(Dog.species)

2. Properties

python
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance   # private by convention

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = value

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError("Insufficient funds")
        self.balance -= amount

acct = BankAccount("Alice", 1000)
acct.deposit(500)
acct.withdraw(200)
print(acct.balance)  # 1300

3. @dataclass

python
from dataclasses import dataclass, field

@dataclass
class Point:
    x: float
    y: float

    def distance_to(self, other):
        return ((self.x-other.x)**2 + (self.y-other.y)**2)**0.5

p1 = Point(0, 0)
p2 = Point(3, 4)
print(p1.distance_to(p2))  # 5.0
print(p1)                  # Point(x=0, y=0)

4. Common Mistakes

  1. Forgetting self as the first parameter of instance methods.
  2. Using a mutable default (list/dict) in __init__ signature — use field(default_factory=list) with dataclass.
  3. Confusing class attributes (shared by all instances) with instance attributes (unique per instance).

Week 8 — OOP Part 2: Inheritance & Dunder Methods

Extend classes with inheritance, call parent methods with super(), implement polymorphism, and customize object behavior with dunder (magic) methods.

1. Inheritance

python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        raise NotImplementedError

class Dog(Animal):
    def speak(self):
        return f"{self.name}: Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name}: Meow!"

for a in [Dog("Fido"), Cat("Whiskers")]:
    print(a.speak())

2. Abstract Base Classes

python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

    @abstractmethod
    def perimeter(self) -> float: ...

    def __str__(self):
        return f"{self.__class__.__name__}: area={self.area():.2f}"

class Circle(Shape):
    def __init__(self, r): self.r = r
    def area(self): return 3.14159 * self.r ** 2
    def perimeter(self): return 2 * 3.14159 * self.r

class Rectangle(Shape):
    def __init__(self, w, h): self.w, self.h = w, h
    def area(self): return self.w * self.h
    def perimeter(self): return 2 * (self.w + self.h)

shapes = [Circle(5), Rectangle(3, 4)]
for s in shapes: print(s)

3. Dunder Methods

python
class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __abs__(self):
        return (self.x**2 + self.y**2)**0.5

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)   # Vector(4, 6)
print(abs(v2))   # 5.0

4. Common Mistakes

  1. Forgetting to call super().__init__() in the child — parent attributes won't be initialized.
  2. Overriding __eq__ without __hash__ makes the object unhashable (can't be used in sets/dicts).
  3. Multiple inheritance with diamond problem — Python's MRO (C3 linearization) resolves this, but be cautious.

Week 9 — Regular Expressions

Unlock powerful text-matching with regular expressions. Learn pattern syntax, groups, lookahead/lookbehind, and use re.findall, re.sub, and re.fullmatch for text extraction and cleaning.

1. Pattern Basics

PatternMatches
.Any character (except newline)
\dDigit [0-9]
\wWord char [a-zA-Z0-9_]
\sWhitespace
^Start of string
$End of string
*0 or more
+1 or more
?0 or 1
{n,m}n to m times
[abc]Any of a, b, c
(abc)Group

2. re Functions

python
import re

text = "Call us at 010-1234-5678 or 02-987-6543"

# findall: return all matches
phones = re.findall(r"\d{2,3}-\d{3,4}-\d{4}", text)
print(phones)   # ['010-1234-5678', '02-987-6543']

# sub: replace
masked = re.sub(r"\d{4}$", "****", phones[0])
print(masked)   # 010-1234-****

# Named groups
m = re.search(r"(?P<area>\d{2,3})-(?P<number>\d+-\d+)", text)
print(m.group("area"), m.group("number"))

3. Common Patterns

python
EMAIL   = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
URL     = r"https?://[\w./%-]+"
DATE    = r"\d{4}-\d{2}-\d{2}"
IP_ADDR = r"\b(?:\d{1,3}\.){3}\d{1,3}\b"

for pattern, sample in [
    (EMAIL, "user@example.com"),
    (URL, "https://python.org/docs"),
    (DATE, "2024-05-15"),
]:
    m = re.fullmatch(pattern, sample)
    print(f"{'OK' if m else 'FAIL'} — {sample}")

4. Common Mistakes

  1. Forgetting to use raw strings (r'...') — backslash sequences get misinterpreted.
  2. Greedy quantifiers match as much as possible. Use *? or +? for non-greedy matching.
  3. re.match() only matches at the start; re.search() scans the whole string.

Week 10 — Intermediate Capstone: Text Analysis Tool

Combine the intermediate track skills to build a command-line text analysis tool that reads files, tokenizes text, computes statistics, and produces a formatted report.

1. Project Specification

bash
python analyze.py [OPTIONS] FILE

Options:
  --top N       Show top N words (default 10)
  --format FMT  Output format: text|csv|json (default text)
  --no-stop     Exclude common stop words
  • Total words, unique words, average word length
  • Top-N word frequency (with ASCII bar chart)
  • Sentence count, average sentence length
  • Most common bigrams
  • Readability score (Flesch Reading Ease approximation)

2. Architecture

  • Tokenizer class — regex-based word/sentence splitter
  • TextStats dataclass — holds all computed metrics
  • Analyzer class — orchestrates tokenization and metric computation
  • Reporter class — formats and prints TextStats (text/csv/json)
  • main() — argparse + Analyzer + Reporter pipeline

3. Sample Output

text
$ python analyze.py hamlet.txt --top 5

File: hamlet.txt  (5447 words, 1234 unique)

Top 5 words:
  the     ████████████████  250
  and     ██████████████    215
  to      ████████████      180
  of      ██████████        150
  a       ████████          130

Sentences: 423  (avg 12.9 words)
Readability: 68.4 (Standard)

💻 Examples

Run these examples and check the output yourself.

01_bank_account.py— BankAccount with full transaction history
CODE
class BankAccount:
    def __init__(self, owner):
        self.owner = owner
        self._balance = 0.0
        self._history = []

    @property
    def balance(self): return self._balance

    def deposit(self, amount):
        if amount <= 0: raise ValueError("Amount must be positive")
        self._balance += amount
        self._history.append(f"+{amount:.2f}")

    def withdraw(self, amount):
        if amount > self._balance: raise ValueError("Insufficient funds")
        self._balance -= amount
        self._history.append(f"-{amount:.2f}")

    def statement(self):
        print(f"Account: {self.owner}")
        for tx in self._history:
            print(f"  {tx}")
        print(f"  Balance: {self._balance:.2f}")

acct = BankAccount("Alice")
acct.deposit(1000)
acct.deposit(500)
acct.withdraw(200)
acct.statement()
▶ Output
Account: Alice
  +1000.00
  +500.00
  -200.00
  Balance: 1300.00
01_shapes.py— Shape hierarchy with polymorphic area/perimeter
CODE
import math
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self): ...
    @abstractmethod
    def perimeter(self): ...
    def __str__(self):
        return f"{type(self).__name__}(area={self.area():.2f}, perim={self.perimeter():.2f})"

class Circle(Shape):
    def __init__(self, r): self.r = r
    def area(self): return math.pi * self.r**2
    def perimeter(self): return 2 * math.pi * self.r

class Rect(Shape):
    def __init__(self, w, h): self.w, self.h = w, h
    def area(self): return self.w * self.h
    def perimeter(self): return 2*(self.w+self.h)

for s in [Circle(5), Rect(3, 4), Circle(1)]:
    print(s)
▶ Output
Circle(area=78.54, perim=31.42)
Rect(area=12.00, perim=14.00)
Circle(area=3.14, perim=6.28)
01_log_parser.py— Extract structured data from an Apache log line
CODE
import re

LOG = '127.0.0.1 - frank [10/Oct/2024:13:55:36 -0700] "GET /index.html HTTP/1.1" 200 2326'

pattern = re.compile(
    r'(?P<ip>[\d.]+) \S+ (?P<user>\S+) '  
    r'\[(?P<time>[^\]]+)\] '
    r'"(?P<method>\S+) (?P<path>\S+) [^"]+"'
    r' (?P<status>\d{3}) (?P<size>\d+)'
)

m = pattern.match(LOG)
if m:
    for key, val in m.groupdict().items():
        print(f"  {key:<8}: {val}")
▶ Output
  ip      : 127.0.0.1
  user    : frank
  time    : 10/Oct/2024:13:55:36 -0700
  method  : GET
  path    : /index.html
  status  : 200
  size    : 2326

📝 Exercises

Try them yourself first, then open the solution to compare.

Exercise 1

Student class

Goal: Model a student with name, scores list, and a computed grade property.

Requirements
  • __init__(name)
  • add_score(score) method
  • average property (float)
  • grade property: A/B/C/D/F based on average
  • __repr__ returning Student(name, avg=XX.X, grade=X)
▶Toggle solution
SOLUTION
class Student:
    def __init__(self, name):
        self.name = name
        self._scores = []

    def add_score(self, score):
        self._scores.append(score)

    @property
    def average(self):
        return sum(self._scores) / len(self._scores) if self._scores else 0.0

    @property
    def grade(self):
        a = self.average
        if a >= 90: return "A"
        if a >= 80: return "B"
        if a >= 70: return "C"
        if a >= 60: return "D"
        return "F"

    def __repr__(self):
        return f"Student({self.name!r}, avg={self.average:.1f}, grade={self.grade})"

s = Student("Alice")
for sc in [90, 85, 92]: s.add_score(sc)
print(s)
▶ Output
Student('Alice', avg=89.0, grade=B
Exercise 2

Fraction class

Goal: Implement a Fraction class with arithmetic operators.

Requirements
  • __init__(numerator, denominator)
  • Reduce to lowest terms using gcd
  • __add__, __sub__, __mul__, __truediv__
  • __str__ → '3/4', __repr__ → Fraction(3, 4)
  • __eq__ for comparison
▶Toggle solution
SOLUTION
from math import gcd

class Fraction:
    def __init__(self, n, d):
        if d == 0: raise ZeroDivisionError
        g = gcd(abs(n), abs(d))
        self.n, self.d = n//g * (1 if d>0 else -1), abs(d)//g

    def __repr__(self): return f"Fraction({self.n}, {self.d})"
    def __str__(self): return f"{self.n}/{self.d}"
    def __add__(self, o): return Fraction(self.n*o.d + o.n*self.d, self.d*o.d)
    def __sub__(self, o): return Fraction(self.n*o.d - o.n*self.d, self.d*o.d)
    def __mul__(self, o): return Fraction(self.n*o.n, self.d*o.d)
    def __truediv__(self, o): return Fraction(self.n*o.d, self.d*o.n)
    def __eq__(self, o): return self.n == o.n and self.d == o.d

a, b = Fraction(1,2), Fraction(1,3)
print(a+b, a-b, a*b, a/b)
▶ Output
5/6 1/6 1/6 3/2
Exercise 3

Email Extractor & Validator

Goal: Extract all email addresses from a block of text and validate each one.

Requirements
  • Use re.findall with EMAIL pattern
  • Validate: local part ≤ 64 chars, domain ≤ 255 chars
  • Print valid/invalid for each found address
▶Toggle solution
SOLUTION
import re

EMAIL = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'

text = "Contact admin@example.com or info@co.uk. Invalid: @bad, x@"
emails = re.findall(EMAIL, text)
for e in emails:
    local, domain = e.split("@", 1)
    if len(local) <= 64 and len(domain) <= 255:
        print(f"  VALID:   {e}")
    else:
        print(f"  INVALID: {e}")
▶ Output
  VALID:   admin@example.com
  VALID:   info@co.uk
Exercise 4

Build the analyzer

Goal: Implement the text analysis tool with all features above.

Requirements
  • All metrics computed correctly
  • argparse with all flags
  • text/csv/json output formats
  • Unit tests for Tokenizer
Grading
  • · Metrics correct — 40%
  • · argparse flags work — 20%
  • · Output formats — 20%
  • · Tests pass — 20%
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗

Comments 0

No comments yet. Be the first.

Only members can comment.

Comments with ads, abuse, personal information or illegal content are hidden or removed. Comments are shown with your nickname. Privacy Policy