Skip to content

Advanced Functions and OOP

By the end of this lesson you'll be able to:

  • Use map(), filter(), any(), all(), chr(), ord(), dir(), and help() effectively
  • Create custom exception classes and use exception chaining
  • Define properties with @property and use dunder methods to customize object behavior
  • Apply object composition to build complex objects from simpler ones

This chapter fills in the remaining advanced tools from Python's functional and OOP toolkit.

Welcome to Chapter 31!

Monty waving welcome This is where your Python skills reach professional level! Custom exceptions, properties, dunder methods — these are the tools that separate beginner code from expert code. Let's level up! Let's code it together!

map() and filter() with Named Functions

You saw map() and filter() with lambdas in Chapter 22. They work just as well with named functions:

1
2
3
4
5
6
7
8
9
def is_even(n):
    return n % 2 == 0

def square(n):
    return n * n

numbers = [1, 2, 3, 4, 5, 6]
evens   = list(filter(is_even, numbers))   # [2, 4, 6]
squares = list(map(square, numbers))       # [1, 4, 9, 16, 25, 36]

This style is more readable than lambdas for complex logic.

any() and all()

any(iterable) returns True if at least one item is truthy. all(iterable) returns True if every item is truthy.

Both short-circuit — they stop as soon as the answer is known.


  

chr() and ord() — Characters and ASCII Codes

ord(character) converts a character to its ASCII (Unicode) integer code. chr(integer) does the reverse — converts an integer to its character.

1
2
3
4
print(ord("A"))   # 65
print(ord("a"))   # 97
print(chr(65))    # "A"
print(chr(9829))  # "♥" — Unicode heart

  

dir() and help() — Exploring Objects

dir(obj) returns a list of all attributes and methods available on obj. help(obj) prints detailed documentation.

These are your best friends when exploring an unfamiliar module or object:

1
2
3
import math
print(dir(math))         # see all math functions
help(math.sqrt)          # full docs for sqrt

id() and hash()

id(obj) returns the unique memory address of an object. Two variables with the same id are the same object — not just equal, but literally the same in memory.

hash(obj) returns a hash value (an integer fingerprint). Dictionaries use hashes for fast lookups. Only immutable objects (strings, numbers, tuples) can be hashed.

What Do You Think Will Happen?

Monty thinking The code below compares is vs == for strings. Will the small strings share the same id? Make your guess — then run it!


  

Custom Exception Classes

You can create your own exception types by subclassing Exception. Custom exceptions make your error messages much clearer to users of your code.

1
2
3
4
5
6
7
class InvalidAgeError(ValueError):
    pass

class NegativeBalanceError(Exception):
    def __init__(self, amount):
        super().__init__(f"Cannot withdraw ${amount} — balance would go negative.")
        self.amount = amount

  

Static Methods

A static method belongs to the class but doesn't need access to self (the instance). Decorate it with @staticmethod. Use it for utility functions that logically belong to the class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Temperature:
    @staticmethod
    def celsius_to_fahrenheit(c):
        return (c * 9/5) + 32

    @staticmethod
    def fahrenheit_to_celsius(f):
        return (f - 32) * 5/9

print(Temperature.celsius_to_fahrenheit(100))   # 212.0

Properties with @property

A property lets you define a method that behaves like an attribute. This lets you add validation or computation to attribute access without changing the interface.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative.")
        self._radius = value

    @property
    def area(self):
        return 3.14159 * self._radius ** 2

c = Circle(5)
print(c.radius)   # calls the getter
print(c.area)     # computed property
c.radius = 10     # calls the setter (validates!)

Dunder Methods Overview

Dunder methods (also called "magic methods") are special methods with double underscores on both sides. You've already seen __init__ and __str__. There are many more:

Method Called when
__len__(self) len(obj)
__add__(self, other) obj + other
__eq__(self, other) obj == other
__lt__(self, other) obj < other
__contains__(self, item) item in obj
__iter__(self) for x in obj
__getitem__(self, key) obj[key]

By implementing dunders, your custom classes work naturally with Python operators and built-in functions.

Object Composition

Object composition means building a complex object from simpler objects — storing instances of one class inside another.

This is often better than deep inheritance hierarchies:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Engine:
    def __init__(self, horsepower):
        self.hp = horsepower

    def start(self):
        print(f"Engine ({self.hp} hp) running.")

class Car:
    def __init__(self, model, horsepower):
        self.model  = model
        self.engine = Engine(horsepower)   # composition!

    def drive(self):
        self.engine.start()
        print(f"{self.model} is moving.")

car = Car("Python GT", 300)
car.drive()

Composition over Inheritance

Monty with a tip Professional Python developers prefer composition over deep inheritance. Instead of ElectricCar(Car(Vehicle)), build ElectricCar with an engine attribute. It's easier to change individual parts without breaking unrelated code.

Learning Check

Your Turn — Add a Dunder Method

Monty thinking The Bag class below stores items, but len(bag) doesn't work yet. Add the __len__ dunder method so len(bag) returns the number of items!


  

Experiments

Try these changes. Predict what will happen first, then run it to check!

  1. Use any() to check if any string in a list has more than 10 characters. You'll know it worked when the result is True or False based on the actual lengths.

  2. Write a Caesar cipher decryption function (shift by -3 instead of +3). Encrypt then decrypt "Hello". You'll know it worked when you get back the original "Hello".

  3. Add __add__ to the Bag class so bag1 + bag2 returns a new Bag with all items from both. You'll know it worked when len(bag1 + bag2) equals len(bag1) + len(bag2).

  4. Add @property area to a Rectangle class. Ensure it recomputes every time width or height changes. You'll know it worked when changing r.width = 10 updates r.area automatically.

  5. Create a Stack class using composition (it has a list attribute) with push, pop, peek, and __len__. You'll know it worked when all four operations work as expected.

Python Expert!

Monty celebrating You've reached the advanced Python tier! Custom exceptions, properties, dunders, and composition are tools used by professional Python developers every day. The last chapters take you into data science and machine learning territory. Let's keep coding!

Take the Chapter Review Quiz

See Annotated References