Programmer’s Python: Everything Is An Object

Chapter 1 – Getting Started

Main Idea: Python is different from many languages because everything is built around objects and references.

Key Notes:

  • Python is high-level and easy to read.

  • Basic Python includes:

    • Strings → text

    • Data structures → lists, tuples, dictionaries, sets

    • Loops → for, while

    • Conditionals → if, elif, else

  • Indentation matters in Python — spacing controls code blocks.

  • Variables do not store raw values directly the same way some lower-level languages do.

  • Instead, variables usually refer to objects in memory.

  • Functions and classes are introduced as things you can create and use like any other object.

  • Python is considered “Pythonic” when code is clean, readable, and simple.

Important Takeaway: In Python, you should think of code as working with objects and references, not just “variables holding values.”

Chapter 2 – Variables, Objects, and Attributes

Main Idea: A variable is really just a name attached to an object.

Key Notes:

  • Variables are references, not boxes.

  • Example:

x = 5
y = x
  • Both x and y can point to the same object.

  • Objects can have attributes (extra data or properties attached to them).

  • Python lets you create and use dynamic attributes.

  • Objects can contain other objects (nested objects).

  • Python uses namespaces to organize names and avoid conflicts.

  • Modules also act like namespaces.

Important Terms:

  • Object = actual thing in memory

  • Reference = a variable name pointing to that object

  • Attribute = data or behavior attached to an object

  • Namespace = a container of names

Important Takeaway: Variables don’t “own” values — they point to objects.

Chapter 3 – The Function Object

Main Idea: Functions in Python are objects too.

Key Notes:

  • A function can be:

    • stored in a variable

    • passed into another function

    • returned from another function

  • Example:

def greet():
    print("hi")

x = greet
x()
  • Python uses pass-by-object-reference.

  • You pass the object reference, not a copy of the variable itself.

  • Lambda expressions create short anonymous functions:

square = lambda x: x * x
  • Functions can even refer to themselves.

Important Takeaway: A Python function is not just code — it is a real object you can store and manipulate.

Chapter 4 – Scope, Lifetime, and Closure

Main Idea: This chapter explains where variables exist, how long they last, and how functions can “remember” data.

Key Notes:

  • Local variables exist only inside a function.

  • Global variables exist outside functions.

  • global lets you change a global variable inside a function.

  • nonlocal lets an inner function change a variable in an outer function.

  • Inner functions = functions inside other functions.

  • Closures happen when a function remembers variables from where it was created.

Example of Closure:

def outer(x):
    def inner():
        return x
    return inner
  • inner() remembers x.

Why Closures Matter:

  • Useful for:

    • keeping state

    • creating customized functions

    • decorators later on

Important Takeaway: Functions can carry data with them, even after the outer function is done.

Chapter 5 – Advanced Functions

Main Idea: Functions can be made much more flexible and powerful.

Key Notes:

  • Default parameters

def greet(name="Guest"):
    print(name)
  • Keyword arguments

greet(name="Bob")
  • Variable-length arguments

def add(*nums):
    return sum(nums)
  • Keyword variable arguments

def info(**kwargs):
    print(kwargs)
  • Unpacking

nums = [1, 2, 3]
print(*nums)
  • Functions can return multiple values.

  • Functions can have docstrings and annotations.

  • This chapter also introduces decorators.

Decorators: A decorator wraps a function to add extra behavior.

Example:

@decorator_name
def my_function():
    pass

Important Takeaway: Python functions are powerful because they are flexible objects that can be passed around, customized, and wrapped.

Chapter 6 – Class, Methods, and Constructors

Main Idea: Classes are also objects, and methods are just functions with special behavior.

Key Notes:

  • A class is a blueprint for making objects.

  • An instance is an object created from a class.

  • Example:

class Dog:
    def __init__(self, name):
        self.name = name
  • __init__ is the initializer (often casually called a constructor).

  • self refers to the current object.

  • Instance attributes belong to each object.

  • A method is just a function inside a class.

  • Python automatically binds methods to objects.

Important Concepts:

  • Class for behavior

  • Instance for data

Important Takeaway: Classes themselves are objects, and methods are really just functions that get attached and bound to objects.

Chapter 7 – Inside Class

Main Idea: This chapter looks deeper into how objects are created.

Key Notes:

  • __new__ creates the object first.

  • __init__ sets it up after creation.

  • So object creation happens in two steps:

    1. create object → __new__

    2. initialize object → __init__

Special Ideas:

  • Singleton pattern:

    • only one object of a class should exist

  • Useful for controlling object creation.

  • Immutable objects (like strings, tuples) may need __new__ more than __init__.

  • Objects can sometimes be made callable like functions.

Example:

class Test:
    def __call__(self):
        print("Called!")

Important Takeaway: Python classes don’t magically create objects — there’s a real creation process happening underneath.

Chapter 8 – Meeting Metaclass

Main Idea: A metaclass is basically a class that creates classes.

Key Notes:

  • If:

    • a class creates objects,

    • then a metaclass creates classes

  • Python’s built-in metaclass is usually type.

Example:

class Dog:
    pass

print(type(Dog))

Output:

<class 'type'>

That means:

  • Dog is an object

  • created by type

Metaclass Uses:

  • control how classes are built

  • enforce rules

  • automatically add methods/attributes

  • create “final” or restricted classes

  • advanced frameworks often use them

Important Methods:

  • __new__

  • __init__

  • __prepare__

Important Takeaway: In Python, classes are objects too, and metaclasses are what make that possible.

Chapter 9 – Advanced Attributes

Main Idea: This chapter explains how to control access to object data.

Key Notes:

  • Python has name mangling for “private” attributes:

self.__secret
  • This makes it harder (not impossible) to access directly.

  • Python doesn’t have true private variables like some languages — it uses conventions and name handling.

  • Properties let you control access to attributes.

Example:

class Person:
    def __init__(self):
        self._age = 0

    @property
    def age(self):
        return self._age

Why Properties Matter:

  • Let you make attributes act like methods

  • Help with:

    • validation

    • cleaner code

    • safer data access

Important Takeaway: Attributes in Python are flexible, and you can control how they are read or changed.

Chapter 10 – Custom Attribute Access

Overview

Python’s attribute model lets you intercept and customize how attribute lookup, assignment, and deletion behave. This chapter explains how Python resolves attribute access and how you can plug into that mechanism.

Descriptor Objects

Python implements the core of attribute access using descriptors — objects that define one or more of:

  • __get__(self, obj, type=None)

  • __set__(self, obj, value)

  • __delete__(self, obj)

A descriptor is any object assigned to a class attribute that implements these methods. For example:

class ValueLogger:
    def __init__(self, name):
        self.name = name

    def __get__(self, obj, obj_type=None):
        print(f"Accessing {self.name}")
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        print(f"Setting {self.name} = {value}")
        obj.__dict__[self.name] = value

Descriptors are the foundation for Python’s built‑in features such as property, methods, and classmethod/staticmethod behavior.

Customizing Attribute Access

Override __getattribute__(self, name) on your class to intercept all attribute lookup. Be careful: you must delegate to the base implementation to avoid infinite recursion:

class Intercept:
    def __getattribute__(self, name):
        print(f"Looking up {name!r}")
        return super().__getattribute__(name)

Customizing Assignment

To intercept assignment, override __setattr__. For example, to enforce read‑only semantics:

class ReadOnly:
    def __init__(self, value):
        self._value = value

    def __setattr__(self, name, value):
        if name.startswith("_"):
            super().__setattr__(name, value)
        else:
            raise AttributeError("Read‑only attribute")

Default Methods

Python provides default implementations for many lookup operations; understanding them helps you decide when to override.

Deleting Attributes

Override __delattr__ to control behavior when del obj.attr is used.

Custom Dir

Override __dir__ to customize what dir(obj) returns.

Slots

Using __slots__ can restrict attribute creation and save memory by preventing the creation of __dict__.

Index/Key Access

Python also decouples attribute vs. item access:

  • Attribute access: obj.attr

  • Item access: obj[key]

Implement __getitem__, __setitem__, and __delitem__ to define dictionary‑like behaviors.

Summary

This chapter shows how Python’s attribute system works under the hood and how you can customize attribute behaviors via descriptors and magic methods.

Chapter 11 – Single Inheritance

Overview

Inheritance enables you to build a class hierarchy where a subclass reuses and extends the behavior of a base class. This chapter explains how single inheritance works and how Python resolves attribute and method lookup. :contentReference[oaicite:1]{index=1}

Basic Inheritance

Define a subclass by specifying a base class in parentheses:

class Base:
    def greet(self):
        print("Hello from Base")

class Sub(Base):
    pass

The subclass automatically inherits any public method or attribute.

Overriding Methods

A subclass can provide a replacement implementation:

class Sub(Base):
    def greet(self):
        print("Hello from Sub")

Using super()

The built‑in super() function makes it easy to delegate to a parent’s method without naming the base class explicitly:

class Sub(Base):
    def greet(self):
        super().greet()
        print("…and hello from Sub!")

Python Method Resolution Order (MRO)

In single inheritance the lookup path is linear, and Python consults the class’s MRO when resolving attribute and method names.

Inheriting Instance Attributes

Subclass initializers (__init__) can extend the base class’s initialization:

class Sub(Base):
    def __init__(self, value):
        super().__init__()
        self.value = value

Customizing Subclassing

You can control subclass behavior via metaclasses and custom hooks (for example, using __init_subclass__ to perform checks on subclass creation).

Metaclass Inheritance

When a class has a custom metaclass, inheritance still applies at the class object creation level; the metaclass’s behavior participates in constructing the subclass.

Summary

Single inheritance is straightforward: subclasses extend or override base class functionality and can use super() to coordinate with ancestor behavior.

Chapter 12 – Multiple Inheritance

Overview

Python supports multiple inheritance — a class can have more than one base. This chapter covers the motivations and the rules that govern method resolution in the presence of multiple parents. :contentReference[oaicite:2]{index=2}

Why Multiple Inheritance?

It lets you combine behavior from several sources. For example:

class A:
    def method(self): print("A")

class B:
    def method(self): print("B")

class C(A, B): pass

The question then becomes: which parent’s `method` should be called?

The Problem with Multiple Inheritance

Ambiguity arises when multiple parents implement the same attribute or method.

Linearization

Python uses a deterministic ordering called linearization to flatten the inheritance graph into a sequence. This ensures each class appears only once and respects parent order.

C3 Algorithm and the MRO

Python’s MRO is computed using the C3 linearization algorithm. It produces an order that preserves the local precedence ordering of base classes and their ancestors.

Cooperative super()

To make multiple inheritance work well, each class should use super() rather than naming the parent explicitly. This lets the next method in the MRO be called cooperatively.

Dwindling Parameter Pattern

When overriding methods with different signatures across the hierarchy, use *args/**kwargs to accept parameters flexibly.

Summary

Multiple inheritance is powerful but requires careful design; Python’s MRO ensures consistent lookup order when classes have multiple parents.

Chapter 13 – Class and Type

Overview

This chapter explores Python’s approach to type and how it relates to classes and instances. It clarifies what “type” means in Python and how strong vs. dynamic typing is realized. :contentReference[oaicite:3]{index=3}

Three Types of Type

There are multiple notions of type in programming:

  • The runtime type of an object (type(obj))

  • The static annotated type (type hints)

  • Conceptual type based on behavior

Primitive Data Types

Built‑in types like int, str, and float have fixed behaviors but are still Python objects.

Class‑Based Type

Every class defines a type. When you create a class, you create a new type.

Hierarchical Typing

Python’s inheritance hierarchy means types can participate in subclass relationships. For example:

isinstance(obj, Base)

Instance and Subclass

isinstance and issubclass help test whether an object belongs to a type or whether a class is derived from another.

Duck Typing

Python generally uses duck typing — if an object has the right methods and attributes, its type often doesn’t matter for correctness:

def quack(duck):
    duck.quack()

As long as duck has a quack method, this works irrespective of actual type.

Summary

Python’s notion of type combines runtime objects, class definitions, and behavioral expectations rather than rigid compile‑time constraints.