Object-Oriented Programming

Beginner-Friendly Teaching Edition
What you will learn: classes and objects, constructors, methods, composition, properties, internal attributes, class attributes, inheritance, polymorphism, duck typing, operator overloading, exceptions, named tuples, data classes, doctest, namespaces, and time-series regression.

Chapter Roadmap

Foundation
Classes, objects, attributes, methods
Data Access
Properties and naming conventions
Reuse
Composition and inheritance
Flexibility
Polymorphism and duck typing
Advanced
Operators, exceptions, data classes
Data Science
Time series and regression

10.1 Introduction

Python is an object-oriented language and everything in Python is an object. Objects are created from classes, like houses are built from blueprints.

ClassObjectAttributes + Methods

This chapter focuses on creating useful custom classes and on classes, objects, inheritance and polymorphism. It also explains that much Python programming is object-based: we commonly reuse classes supplied by Python and third-party libraries.

Big idea: A class is a new data type. An object is an instance of that class.

10.2 Custom Class Account

The chapter begins with a bank Account class. It stores a name and balance and provides methods such as deposit().

Test-driving Account

from account import Account
from decimal import Decimal

account1 = Account('John Green', Decimal('50.00'))
account1.name
account1.balance

account1.deposit(Decimal('25.53'))
account1.balance
John Green
Decimal('50.00')
Decimal('75.53')

Account(...) is a constructor expression. It creates and initializes an object.

Account class definition

class Account:
    def __init__(self, name, balance):
        if balance < Decimal('0.00'):
            raise ValueError('Initial balance must be >= to 0.00.')
        self.name = name
        self.balance = balance

    def deposit(self, amount):
        if amount < Decimal('0.00'):
            raise ValueError('amount must be positive.')
        self.balance += amount
  • class starts a class definition.
  • __init__ initializes an object.
  • self refers to the current object.
  • self.name and self.balance are object attributes.

Composition

An Account's attributes can refer to objects of other classes, such as a string for the name and a Decimal for the balance. This is composition, also called a has-a relationship.

10.3 Controlling Access to Attributes

Direct attribute assignment can bypass validation:

account1.balance = Decimal('-1000.00')
Ordinary Python attributes do not automatically validate values assigned to them.

Encapsulation means controlling how client code interacts with an object's internal data. Python relies strongly on conventions rather than absolute private-data enforcement.

  • name → normally public.
  • _name → conventionally internal use.
  • A leading underscore does not make an attribute inaccessible.

10.4 Properties for Data Access

A property looks like an attribute but is implemented using methods. This allows a class to validate and format data.

class Time:
    def __init__(self, hour=0, minute=0, second=0):
        self.hour = hour
        self.minute = minute
        self.second = second

    @property
    def hour(self):
        return self._hour

    @hour.setter
    def hour(self, hour):
        if not (0 <= hour < 24):
            raise ValueError(f'Hour ({hour}) must be 0-23')
        self._hour = hour

The getter reads the internal value. The setter validates a new value before storing it. A read-write property has both getter and setter; a read-only property has only a getter.

Time object

wake_up = Time(hour=6, minute=30)
print(wake_up)
wake_up.hour = 7
6:30:00 AM

The chapter also introduces __repr__ for an official-style representation and __str__ for a human-friendly representation.

Design notes

  • The public interface is the set of properties and methods clients should use.
  • Getters can format values.
  • Setters can validate values.
  • Internal representation can change while the public interface stays stable.
  • Internal utility methods conventionally begin with one underscore.

10.5 Simulating "Private" Attributes

Python uses two leading underscores to trigger name mangling and discourage direct client access.

class PrivateClass:
    def __init__(self):
        self.public_data = "public"
        self.__private_data = "private"

my_object.public_data works, while my_object.__private_data raises AttributeError. Python internally changes the name to a form such as _PrivateClass__private_data.

Remember: this is not absolute security; it is a mechanism and convention for avoiding accidental direct access.

10.6 Case Study: Card Shuffling and Dealing Simulation

Card represents one playing card. DeckOfCards represents a 52-card deck containing Card objects.

Card class attributes

class Card:
    FACES = ['Ace', '2', '3', '4', '5', '6',
             '7', '8', '9', '10', 'Jack', 'Queen', 'King']
    SUITS = ['Hearts', 'Diamonds', 'Clubs', 'Spades']

FACES and SUITS are class attributes: the information belongs to the class and is shared by its objects.

String representations

def __repr__(self):
    return f"Card(face='{self.face}', suit='{self.suit}')"

def __str__(self):
    return f'{self.face} of {self.suit}'

The chapter also uses __format__ so Card objects can be formatted inside f-strings.

DeckOfCards

class DeckOfCards:
    NUMBER_OF_CARDS = 52

    def __init__(self):
        self._current_card = 0
        self._deck = []
        for count in range(52):
            self._deck.append(
                Card(Card.FACES[count % 13],
                     Card.SUITS[count // 13])
            )

    def shuffle(self):
        self._current_card = 0
        random.shuffle(self._deck)

    def deal_card(self):
        try:
            card = self._deck[self._current_card]
            self._current_card += 1
            return card
        except:
            return None

The case study then uses Matplotlib, Path, imshow and 52 subplots to display card images.

10.7 Inheritance: Base Classes and Subclasses

Inheritance creates a new class from an existing class. The existing class is the base class or superclass; the new class is the subclass or derived class.

CommunityMember
Employee  Student  Alum
Faculty / Staff

Inheritance represents an is-a relationship. A Car is a Vehicle. A subclass is more specific than its base class.

Memory trick: inheritance = "is-a"; composition = "has-a".

10.8 Building an Inheritance Hierarchy; Introducing Polymorphism

The payroll example uses CommissionEmployee and SalariedCommissionEmployee. The second is a specialized CommissionEmployee with a base salary.

Base class

class CommissionEmployee:
    def earnings(self):
        return self.gross_sales * self.commission_rate

Properties validate gross sales and commission rate.

Subclass and super()

class SalariedCommissionEmployee(CommissionEmployee):
    def __init__(self, first_name, last_name, ssn,
                 gross_sales, commission_rate, base_salary):
        super().__init__(
            first_name, last_name, ssn,
            gross_sales, commission_rate
        )
        self.base_salary = base_salary

    def earnings(self):
        return super().earnings() + self.base_salary

super() calls functionality from the base class. The subclass overrides earnings().

Testing the "is-a" relationship

issubclass(SalariedCommissionEmployee, CommissionEmployee)
isinstance(s, CommissionEmployee)
isinstance(s, SalariedCommissionEmployee)
True
True
True

Polymorphism

employees = [commission_employee, salaried_employee]

for employee in employees:
    print(employee)
    print(employee.earnings())

The same method call can produce different behavior depending on the object's class. This is polymorphism.

10.9 Duck Typing and Polymorphism

Python can use polymorphic behavior even when objects are not related through inheritance. This is duck typing.

Easy meaning: If an object has the method or attribute your code needs, you can often use it. The object's exact type does not have to match.
class WellPaidDuck:
    def __repr__(self):
        return 'I am a well-paid duck'

    def earnings(self):
        return Decimal('1000000.00')

employees = [c, s, WellPaidDuck()]

for employee in employees:
    print(employee)
    print(employee.earnings())

The duck works because it provides the required interface: a usable string representation and an earnings() method.

10.10 Operator Overloading

Operator overloading lets custom classes define how operators such as + and += work.

OperatorSpecial method
+__add__
+=__iadd__
*__mul__

The chapter demonstrates this with a custom Complex class.

def __add__(self, right):
    return Complex(
        self.real + right.real,
        self.imaginary + right.imaginary
    )

def __iadd__(self, right):
    self.real += right.real
    self.imaginary += right.imaginary
    return self

__add__ returns a new object. __iadd__ modifies the left object.

You cannot change operator precedence, grouping rules or operator arity, and you cannot create brand-new operators.

10.11 Exception Class Hierarchy and Custom Exceptions

Exceptions are objects belonging to an exception class hierarchy. The hierarchy begins with BaseException.

BaseException
Exception
ValueError · TypeError · IndexError · KeyError · AttributeError · ...

Other direct BaseException subclasses include SystemExit, KeyboardInterrupt and GeneratorExit.

An except Exception handler can catch exceptions derived from Exception. Specific handlers should come before a broad handler.

You can define custom exceptions by inheriting from Exception, but the chapter recommends using an existing standard exception whenever one is appropriate.

10.12 Named Tuples

A named tuple lets you access tuple members by name instead of numeric index.

from collections import namedtuple

Card = namedtuple('Card', ['face', 'suit'])
card = Card(face='Ace', suit='Spades')

card.face
card.suit
card
'Ace'
'Spades'
Card(face='Ace', suit='Spades')

Named tuples also provide features such as _make() for constructing an object from an iterable and _asdict() for getting an ordered dictionary representation.

10.13 A Brief Intro to Python 3.7's New Data Classes

Data classes provide a concise way to define classes that mainly store related data. The @dataclass decorator can automatically generate common code.

Card data class

from dataclasses import dataclass
from typing import ClassVar, List

@dataclass
class Card:
    FACES: ClassVar[List[str]] = [...]
    SUITS: ClassVar[List[str]] = [...]

    face: str
    suit: str

Data attributes use variable annotations. Class attributes use ClassVar. Python remains dynamically typed; annotations are not enforced at runtime.

c1 = Card(Card.FACES[0], Card.SUITS[3])
c1
c1 == Card(Card.FACES[0], Card.SUITS[3])
Card(face='Ace', suit='Spades')
True

Advantages

  • Automatically generates __init__, __repr__ and __eq__.
  • Can generate ordering methods when requested.
  • Reduces boilerplate and maintenance.
  • Can participate in inheritance.
  • Annotations can help static analysis tools.

10.14 Unit Testing with Docstrings and doctest

The doctest module tests examples written inside docstrings. It searches for statements beginning with >>> and compares actual results with expected output.

def add(a, b):
    """Add two numbers.

    >>> add(2, 3)
    5
    """
    return a + b

if __name__ == '__main__':
    import doctest
    doctest.testmod(verbose=True)

Each example acts like a small unit test. Successful tests can be retested after code changes. IPython provides %doctest_mode to make interactive sessions easier to copy into doctests.

10.15 Namespaces and Scopes

A namespace associates identifiers with objects. Important namespaces include local, global, built-in, enclosing, class and object namespaces.

NamespaceContains
LocalFunction/method parameters and local variables
GlobalModule-level names
Built-inprint, range, int, str, etc.
EnclosingOuter-function names for nested functions
Class/ObjectClass attributes / object attributes

LEGB

For nested functions, Python searches Local → Enclosing → Global → Built-in.

z = 'global z'

def print_variables():
    y = 'local y'
    print(y)
    print(z)

print_variables()
local y
global z

After the function ends, its local namespace disappears, so its local variables are no longer available.

10.16 Intro to Data Science: Time Series and Simple Linear Regression

A time series is a sequence of observations associated with points in time. Examples include stock prices and temperature readings.

  • Univariate: one observation per time.
  • Multivariate: two or more observations per time.
  • Analysis: looks for patterns such as seasonality.
  • Forecasting: uses past data to predict the future.

Simple linear regression

y = mx + b
  • x = independent variable
  • y = dependent/predicted variable
  • m = slope
  • b = intercept

The chapter first illustrates a linear relationship with Fahrenheit and Celsius temperatures.

c = lambda f: 5 / 9 * (f - 32)
temps = [(f, c(f)) for f in range(0, 101, 10)]

import pandas as pd
temps_df = pd.DataFrame(
    temps, columns=['Fahrenheit', 'Celsius']
)
temps_df.plot(x='Fahrenheit', y='Celsius', style='.-')

It then uses New York City January average high temperatures from 1895–2018 and SciPy's stats.linregress.

from scipy import stats

linear_regression = stats.linregress(
    x=nyc.Date,
    y=nyc.Temperature
)

The resulting slope and intercept are used in y = mx + b to predict temperatures. Seaborn's regplot is used to visualize the observations and regression line.

Important: Predictions become less reliable as you move farther outside the historical data range.

10.17 Wrap-Up

You learned how to craft valuable classes, create objects, initialize attributes, define methods and use properties.

You learned composition, internal naming conventions, name mangling, class attributes and special methods such as __repr__, __str__ and __format__.

You then learned inheritance, super(), method overriding, polymorphism, issubclass(), isinstance() and duck typing.

The advanced topics included operator overloading, the exception hierarchy, named tuples, data classes, doctest and namespaces. The data-science section introduced time series and simple linear regression using pandas, SciPy and Seaborn.

Quick Memory Map

Class
Blueprint / new data type
Object
Instance of a class
self
Current object
__init__
Initialize object data
Property
Controlled attribute-like access
Composition
"has-a"
Inheritance
"is-a"
Polymorphism
Same call, different behavior
Duck typing
Required behavior matters
Data class
Less boilerplate
Namespace
Names mapped to objects
LEGB
Local → Enclosing → Global → Built-in

Common Mistakes

  • Forgetting self in instance methods.
  • Returning a non-None value from __init__.
  • Confusing composition ("has-a") with inheritance ("is-a").
  • Forgetting super().__init__() when base initialization is required.
  • Putting a broad exception handler before specific handlers.
  • Assuming type annotations enforce types at runtime.
  • Assuming duck typing requires inheritance.

Revision Questions

  1. What is a class and what is an object?
  2. What is the purpose of __init__?
  3. Why is self used?
  4. What is composition?
  5. Why are properties useful?
  6. What does a leading underscore mean?
  7. What is name mangling?
  8. What is a class attribute?
  9. What is inheritance?
  10. What does super() do?
  11. What is polymorphism?
  12. What is duck typing?
  13. What is operator overloading?
  14. Why does Python have an exception hierarchy?
  15. What is a named tuple?
  16. What does @dataclass provide?
  17. How does doctest work?
  18. What is a namespace?
  19. Explain LEGB.
  20. What are time-series analysis and forecasting?

Practice Programs

  1. Create a Student class with name and marks.
  2. Add a property that rejects invalid marks.
  3. Create a base Employee class and a specialized subclass.
  4. Create two unrelated classes that both implement area() and process them in one loop.
  5. Create a custom class that overloads +.
  6. Convert a simple record class into a data class.
  7. Add doctest examples to a validation function.
  8. Write a nested function and trace LEGB lookup.
  9. Load a small time series with pandas and fit a regression line.