Skip to content

Latest commit

 

History

History
2708 lines (1739 loc) · 35.2 KB

File metadata and controls

2708 lines (1739 loc) · 35.2 KB

Chapter 44 — Attributes and Methods


Learning Objectives

By the end of this chapter, you should understand:

  • What an attribute is.
  • How instance attributes are stored.
  • How class attributes are stored.
  • How Python looks up attributes.
  • Why attribute lookup and attribute assignment are different.
  • How instance attributes can shadow class attributes.
  • How methods are functions stored on classes.
  • What a bound method is.
  • How self is passed automatically.
  • Why calling obj.method() differs from calling Class.method(obj).
  • How getattr(), setattr(), hasattr(), and delattr() work.
  • Why mutable class attributes are dangerous when used as per-instance state.
  • How attribute lookup prepares us for properties, descriptors, inheritance, and MRO.

Chapter 43 introduced classes and instances.

Now we zoom in on the most important operation in object-oriented Python:

attribute access

Every time you write:

user.name
user.greet()
config.DEBUG
math.sqrt

Python is doing attribute lookup.

Understanding this one idea unlocks a surprising amount of advanced Python.

Methods, class attributes, instance attributes, properties, descriptors, inheritance, super(), and the data model all depend on attribute lookup.

This chapter is where the machinery starts to become visible.


Concept Overview

An attribute is a name attached to an object.

Example:

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

Create an instance:

user = User("Ada")

Access:

print(user.name)

Here:

name

is an attribute of the user object.

Conceptually:

user instance namespace:
    name -> "Ada"

The expression:

user.name

means:

look up attribute name on object user

Attributes are how objects expose state and behavior.

State:

user.name

Behavior:

user.greet()

Both use attribute lookup.


Attribute Access Uses Dot Syntax

Dot syntax:

object.attribute

means:

find attribute on object

Examples:

user.name
account.balance
task.done
math.sqrt
settings.DEBUG

The object before the dot can be:

  • An instance.
  • A class.
  • A module.
  • A package.
  • A function.
  • Almost any Python object.

Dot syntax is everywhere because Python's object model is everywhere.

Example:

import math

print(math.pi)
print(math.sqrt(25))

math is a module object.

pi and sqrt are attributes of that module.

Same syntax:

module.attribute
instance.attribute
class.attribute

The details differ, but the idea is the same:

attribute lookup on an object

Instance Attributes

Instance attributes belong to individual instances.

Example:

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

Create:

ada = User("Ada", "ada@example.com")
grace = User("Grace", "grace@example.com")

Each instance has its own attributes:

ada:
    name  -> "Ada"
    email -> "ada@example.com"

grace:
    name  -> "Grace"
    email -> "grace@example.com"

Use:

print(ada.name)
print(grace.name)

Output:

Ada
Grace

Same attribute name.

Different instance namespaces.

This is why classes can create many objects with the same structure but different state.


Inspecting Instance Attributes

For ordinary Python instances, instance attributes are stored in a dictionary called __dict__.

Example:

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email


user = User("Ada", "ada@example.com")

print(user.__dict__)

Output:

{'name': 'Ada', 'email': 'ada@example.com'}

You can also use:

print(vars(user))

Output:

{'name': 'Ada', 'email': 'ada@example.com'}

For many ordinary classes:

vars(user) shows the instance namespace

This is not true for every possible object.

Some classes use __slots__.

Some objects are implemented in C.

Some objects customize attribute access.

But for normal beginner-defined classes, __dict__ is the right model.


Class Attributes

Class attributes belong to the class object.

Example:

class User:
    role = "member"

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

The class namespace contains:

role -> "member"
__init__ -> function object

Access through the class:

print(User.role)

Output:

member

Access through an instance:

ada = User("Ada")
print(ada.role)

Output:

member

Why does ada.role work?

Because if Python does not find role on the instance, it can look on the class.

This is attribute lookup.


Inspecting Class Attributes

Class attributes are stored in the class namespace.

Example:

class User:
    role = "member"

    def greet(self):
        return "hello"

Inspect:

print(User.__dict__)

You will see a mapping containing names such as:

role
greet
__module__
__dict__
__weakref__
__doc__

Some names are added by Python automatically.

You can also use:

print(vars(User))

The important part:

role -> "member"
greet -> function object

A class is an object with a namespace.

Class attributes live there.

Instance attributes live on instances.


Basic Attribute Lookup

For a simple instance attribute lookup:

obj.name

Python roughly asks:

1. Does the instance have this attribute?
2. If not, does the class have this attribute?
3. If not, do parent classes have this attribute?
4. If not, raise AttributeError.

This is simplified.

Descriptors can change the order.

We will study descriptors later.

For now, the beginner model is:

instance first
class next
inherited classes later

Example:

class User:
    role = "member"


ada = User()
ada.name = "Ada"

Lookup:

ada.name

finds name on the instance.

Lookup:

ada.role

does not find role on the instance.

Then it finds role on the class.


Attribute Lookup Example

Code:

class User:
    role = "member"

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


ada = User("Ada")

Namespaces:

ada instance:
    name -> "Ada"

User class:
    role -> "member"
    __init__ -> function object

Expression:

ada.name

Lookup:

find name on ada instance -> yes -> "Ada"

Expression:

ada.role

Lookup:

find role on ada instance -> no
find role on User class -> yes -> "member"

Expression:

ada.missing

Lookup:

find missing on ada instance -> no
find missing on User class -> no
raise AttributeError

Attribute Assignment Is Different From Lookup

This is crucial.

Attribute lookup:

ada.role

can search the instance and then the class.

Attribute assignment:

ada.role = "admin"

usually writes to the instance.

It does not search for role on the class and modify it.

Example:

class User:
    role = "member"


ada = User()
grace = User()

ada.role = "admin"

print(ada.role)
print(grace.role)
print(User.role)

Output:

admin
member
member

Why?

Assignment created:

ada instance:
    role -> "admin"

The class still has:

User class:
    role -> "member"

Lookup and assignment are different operations.

Do not blur them.


Shadowing Class Attributes

When an instance attribute has the same name as a class attribute, it shadows the class attribute for that instance.

Example:

class User:
    role = "member"


ada = User()
grace = User()

ada.role = "admin"

Now:

ada.role

finds the instance attribute first:

admin

But:

grace.role

does not find role on grace, so it finds the class attribute:

member

The class attribute still exists:

print(User.role)

Output:

member

Shadowing means:

a nearer namespace has a name that hides a farther namespace's name during lookup

This is the same concept we saw with local variables shadowing globals and built-ins.


Deleting an Instance Attribute Can Reveal a Class Attribute

Example:

class User:
    role = "member"


ada = User()
ada.role = "admin"

print(ada.role)

del ada.role

print(ada.role)

Output:

admin
member

What happened?

Before deletion:

ada instance:
    role -> "admin"

User class:
    role -> "member"

After:

del ada.role

the instance attribute is removed.

Now lookup:

ada.role

does not find role on the instance.

So it finds role on the class.

Deletion did not create the class attribute.

It revealed the one that was already there.


Mutating a Class Attribute Through an Instance

Assignment and mutation are different.

Consider:

class Team:
    members = []


red = Team()
blue = Team()

red.members.append("Ada")

print(blue.members)

Output:

['Ada']

Why?

Lookup:

red.members

does not find members on red.

It finds members on the class.

That object is a list.

Then:

.append("Ada")

mutates the list.

It does not assign a new members attribute to red.

So both instances still see the shared class list.

This is the classic mutable class attribute trap.


Assignment Creates an Instance Attribute

Compare:

class Team:
    members = []


red = Team()
blue = Team()

red.members = ["Ada"]

Now:

print(red.members)
print(blue.members)
print(Team.members)

Output:

['Ada']
[]
[]

Why?

Assignment:

red.members = ["Ada"]

creates an instance attribute on red.

It does not mutate Team.members.

Namespaces:

red instance:
    members -> ["Ada"]

blue instance:
    no members

Team class:
    members -> []

blue.members still finds the class list.

This contrast is vital:

red.members.append(...) -> lookup then mutate found object
red.members = ...       -> assign attribute on red

Correct Per-Instance Mutable Attributes

If each instance needs its own list, create the list in __init__.

Correct:

class Team:
    def __init__(self, name):
        self.name = name
        self.members = []

    def add_member(self, member):
        self.members.append(member)

Use:

red = Team("Red")
blue = Team("Blue")

red.add_member("Ada")

print(red.members)
print(blue.members)

Output:

['Ada']
[]

Each instance has its own namespace:

red:
    name -> "Red"
    members -> red's list

blue:
    name -> "Blue"
    members -> blue's list

Rule:

mutable state that belongs to one object should usually be created on that object

Methods Are Attributes Too

A method is found through attribute lookup.

Example:

class User:
    def greet(self):
        return "hello"


user = User()

Call:

user.greet()

Before Python calls anything, it must evaluate:

user.greet

That is attribute lookup.

Where is greet stored?

In the class namespace:

User class:
    greet -> function object

When accessed through the instance, Python returns a bound method:

method = user.greet
print(method)

The bound method remembers both:

the function
the instance

Then:

method()

passes the instance as self.


Functions Stored on Classes

Inside a class body:

class User:
    def greet(self):
        return "hello"

the def statement creates a function object.

That function object is stored in the class namespace under the name greet.

Conceptually:

User class namespace:
    greet -> function object

Access through the class:

print(User.greet)

You see a function.

Access through an instance:

user = User()
print(user.greet)

You see a bound method.

The function did not change permanently.

Attribute access transformed it for this access.

That transformation is part of the descriptor protocol.

We will study descriptors later.

For now:

class access gives function
instance access gives bound method

Bound Methods

A bound method is a callable object that remembers an instance.

Example:

class User:
    def greet(self):
        return f"hello from {self.name}"

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


user = User("Ada")
method = user.greet

Now:

print(method())

Output:

hello from Ada

The method object remembers:

function -> User.greet
self     -> user

You can inspect:

print(method.__self__)
print(method.__func__)

__self__ is the bound instance.

__func__ is the original function.

These details are not used every day.

But they prove the model:

bound method = function + instance

obj.method() vs Class.method(obj)

These are usually equivalent for ordinary instance methods:

obj.method()

and:

Class.method(obj)

Example:

class User:
    def greet(self):
        return f"hello {self.name}"

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


ada = User("Ada")

print(ada.greet())
print(User.greet(ada))

Both output:

hello Ada

Why?

ada.greet()

gets a bound method that already remembers ada.

User.greet(ada)

gets the raw function from the class and passes ada manually.

This explains self.

self is not magic inside the function.

It is just the first parameter.

The method call syntax supplies it automatically.


Method Lookup Uses Attributes

Method lookup follows attribute lookup rules.

Example:

class User:
    def describe(self):
        return "user"


user = User()

When you call:

user.describe()

Python:

1. looks for describe on user instance
2. if not found, looks on User class
3. finds function object
4. binds it to user
5. calls bound method

This means methods are not a separate lookup category.

They are attributes that happen to be callable and bind specially when they are functions stored on classes.

This is one of Python's elegant unifications:

data attribute lookup
method lookup
module attribute lookup
class attribute lookup

all use the attribute model.


Overriding a Method on One Instance

Because methods are attributes, you can shadow a method on a single instance.

Example:

class User:
    def greet(self):
        return "hello"


user = User()
user.greet = lambda: "custom hello"

print(user.greet())

Output:

custom hello

What happened?

The instance now has an attribute named greet.

Lookup finds the instance attribute before the class method.

Namespace:

user instance:
    greet -> lambda function

User class:
    greet -> original function

This is flexible.

It can also be confusing.

Do not usually replace methods on individual instances in ordinary application code.

But knowing it is possible helps explain how attribute lookup works.


Callable Attributes Are Not Always Methods

If an instance attribute is callable, you can call it.

Example:

class Button:
    pass


button = Button()
button.action = lambda: "clicked"

print(button.action())

Output:

clicked

But action is not a normal bound method.

It is just a function object stored directly on the instance.

It does not automatically receive self.

Example:

button.action = lambda self: "clicked"
button.action()

This fails because no self is passed automatically for plain functions stored on instances.

Automatic method binding happens for functions found on classes.

This distinction matters.


Attribute Assignment

Attribute assignment:

obj.name = value

usually stores a name on the object.

Example:

class User:
    pass


user = User()
user.name = "Ada"

Now:

print(user.__dict__)

Output:

{'name': 'Ada'}

For ordinary instances, assignment writes to the instance namespace.

But there are advanced exceptions:

  • Properties can intercept assignment.
  • Descriptors can intercept assignment.
  • __setattr__ can customize assignment.
  • __slots__ can restrict available attributes.

We will study these later.

For now:

obj.name = value

usually creates or updates an instance attribute.


Attribute Deletion

Attribute deletion:

del obj.name

usually removes an attribute from the object.

Example:

class User:
    pass


user = User()
user.name = "Ada"

print(user.name)

del user.name

print(user.name)

The last line raises:

AttributeError

because the instance attribute no longer exists.

If a class attribute with the same name exists, deletion can reveal it:

class User:
    role = "member"


user = User()
user.role = "admin"

del user.role

print(user.role)

Output:

member

Again:

delete instance attribute
then lookup can find class attribute

getattr()

getattr() gets an attribute by name.

Example:

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


user = User("Ada")

print(getattr(user, "name"))

Output:

Ada

This is equivalent to:

user.name

when the attribute name is known in code.

getattr() is useful when the attribute name is dynamic:

field = "name"
print(getattr(user, field))

You can provide a default:

print(getattr(user, "email", "missing"))

Output:

missing

Without a default, missing attributes raise AttributeError.


setattr()

setattr() sets an attribute by name.

Example:

class User:
    pass


user = User()

setattr(user, "name", "Ada")

print(user.name)

Output:

Ada

This is equivalent to:

user.name = "Ada"

when the attribute name is known in code.

setattr() is useful for dynamic names:

field = "email"
value = "ada@example.com"

setattr(user, field, value)

Use it carefully.

Dynamic attribute creation can make objects harder to understand if overused.

Clear explicit attributes are usually better.


hasattr()

hasattr() checks whether an object has an attribute.

Example:

class User:
    pass


user = User()
user.name = "Ada"

print(hasattr(user, "name"))
print(hasattr(user, "email"))

Output:

True
False

At a high level:

hasattr(obj, name)

tries to get the attribute.

If lookup succeeds, it returns True.

If lookup raises AttributeError, it returns False.

This means hasattr() can trigger custom attribute logic.

For ordinary objects, that is fine.

For advanced objects with properties or custom __getattr__, remember that attribute checks may run code.


delattr()

delattr() deletes an attribute by name.

Example:

class User:
    pass


user = User()
user.name = "Ada"

delattr(user, "name")

print(hasattr(user, "name"))

Output:

False

This is equivalent to:

del user.name

when the attribute name is known.

Use delattr() for dynamic names:

field = "name"
delattr(user, field)

Again, dynamic attribute manipulation is powerful.

It should be used when the design calls for it, not as a default habit.


dir()

dir() lists names available on an object.

Example:

class User:
    role = "member"

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

    def greet(self):
        return "hello"


user = User("Ada")

print(dir(user))

You may see names such as:

name
role
greet
__class__
__dict__
...

dir() includes more than instance attributes.

It includes names available through the object's type and inheritance.

Use:

vars(user)

to see direct instance attributes.

Use:

dir(user)

to inspect available names more broadly.


Missing Attributes

If an attribute is not found, Python raises AttributeError.

Example:

class User:
    pass


user = User()
print(user.name)

Error:

AttributeError

Namespace explanation:

Python looked for name on the instance
then on the class
then inherited classes
it did not find name

Compare with:

print(name)

That raises:

NameError

because plain name lookup failed.

Different operation:

name       -> plain name lookup
user.name  -> attribute lookup

Different error:

NameError
AttributeError

Attribute Lookup and Inheritance Preview

We have mostly discussed one class.

Inheritance adds parent classes.

Example:

class Animal:
    def speak(self):
        return "sound"


class Dog(Animal):
    pass


dog = Dog()
print(dog.speak())

Output:

sound

Where did speak come from?

Not from the dog instance.

Not from the Dog class.

It came from the parent class Animal.

Inheritance extends attribute lookup:

instance
class
parent classes

For multiple inheritance, the order becomes more interesting.

That is MRO.

We will study it later.


Attribute Lookup and Descriptors Preview

Our simplified model says:

instance first
class next

But descriptors can alter this.

Properties, methods, static methods, class methods, and many advanced Python features rely on descriptors.

Example:

class User:
    @property
    def name(self):
        return "Ada"

Here:

user.name

looks like ordinary attribute access.

But it runs a method behind the scenes.

That is descriptor behavior.

We are not studying descriptors fully yet.

But remember:

attribute lookup is customizable

This is why attribute lookup is central to advanced Python.


Attribute Lookup and Properties Preview

Properties let method logic appear as attribute access.

Example:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    @property
    def area(self):
        return self.width * self.height

Use:

rectangle = Rectangle(10, 5)
print(rectangle.area)

No parentheses.

It looks like data.

But Python calls the property's getter method.

This is possible because attribute lookup has hooks.

Chapter 45 will introduce managed attributes and encapsulation.

Chapter 54 will go deeper into descriptors.

For now, see properties as a preview of the power hidden behind dot syntax.


Attribute Assignment and Properties Preview

Properties can also manage assignment.

Example idea:

class User:
    @property
    def age(self):
        ...

    @age.setter
    def age(self, value):
        ...

Then:

user.age = 30

may run validation code.

This is different from ordinary instance attribute assignment.

So far, our simple rule has been:

obj.name = value writes to obj

That is true for ordinary attributes.

But managed attributes can intercept assignment.

This is why we are moving from:

basic attributes

to:

managed attributes

in the next chapter.


Attribute Access Is Part of API Design

When you expose an attribute, you expose part of an object's interface.

Example:

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

Other code may use:

user.name

That becomes part of how the object is used.

If later you rename it:

user.full_name

you may break callers.

Attributes are not just implementation details once other code depends on them.

Object API includes:

  • Attributes callers read.
  • Attributes callers assign.
  • Methods callers call.
  • Exceptions methods raise.
  • Behavior methods promise.

Designing attributes is designing an interface.


Public and Internal Attributes

Python uses naming conventions.

Public attribute:

user.name

Internal-looking attribute:

user._name

The leading underscore means:

this is intended for internal use

It is not strict privacy.

Other code can still access:

user._name

But the convention says:

do not rely on this from outside the class

This convention matters because Python does not force heavy access restrictions.

Python relies on clear interfaces and responsible usage.

Chapter 45 will discuss encapsulation more carefully.


A Complete Example: User

Class:

class User:
    role = "member"

    def __init__(self, name, email):
        self.name = name
        self.email = email

    def display(self):
        return f"{self.name} <{self.email}>"

Use:

ada = User("Ada", "ada@example.com")

Namespaces:

ada instance:
    name  -> "Ada"
    email -> "ada@example.com"

User class:
    role    -> "member"
    __init__ -> function object
    display -> function object

Lookups:

ada.name

finds instance attribute.

ada.role

finds class attribute.

ada.display

finds function on class and returns bound method.

ada.display()

calls the bound method.

This one example shows the whole chapter in miniature.


A Complete Example: Counter

Class:

class Counter:
    step = 1

    def __init__(self):
        self.value = 0

    def increment(self):
        self.value += self.step
        return self.value

Use:

counter = Counter()

print(counter.increment())
print(counter.increment())

Output:

1
2

Lookup inside increment:

self.value

finds instance attribute.

self.step

does not find step on the instance, so it finds class attribute.

Now:

counter.step = 5

This creates an instance attribute.

Next:

print(counter.increment())

uses instance step, not class step.

This is a practical use of class defaults that instances can override.


A Complete Example: Plugin Registry

Class attributes can represent shared data when sharing is intentional.

Example:

class PluginRegistry:
    plugins = {}

    @classmethod
    def register(cls, name, plugin):
        cls.plugins[name] = plugin

We have not deeply studied @classmethod yet.

But the idea is:

plugins belongs to the class, not one instance

Shared class attributes are appropriate when the data is truly shared.

Bad:

per-team members list stored on Team class

Good:

one registry shared by the registry class

The issue is not mutability itself.

The issue is accidental sharing.

Mutable class attributes are fine when shared mutation is the design.

They are dangerous when each instance should have its own state.


Common Mistake: Thinking obj.attr = value Updates the Class

Example:

class User:
    role = "member"


user = User()
user.role = "admin"

This does not update:

User.role

It creates or updates:

user.role

Check:

print(User.role)
print(user.role)

Output:

member
admin

If you want to update the class attribute:

User.role = "admin"

Then instances without their own role will see the new class value.

Attribute assignment target matters.


Common Mistake: Mutating Shared Class Data Accidentally

Bad:

class Cart:
    items = []

    def add(self, item):
        self.items.append(item)

Every cart shares the same list.

Correct:

class Cart:
    def __init__(self):
        self.items = []

    def add(self, item):
        self.items.append(item)

Now each cart has its own list.

Ask:

Should this data be shared by all instances?

If yes, class attribute may be right.

If no, use instance attribute.


Common Mistake: Forgetting Methods Are Attributes

Beginners often think methods are stored on each instance.

For normal methods, they are not.

Example:

class User:
    def greet(self):
        return "hello"

The function greet is stored on the class.

Instances access it through attribute lookup.

This means:

User.greet

and:

user.greet

are related but not identical.

Class access gives the function.

Instance access gives a bound method.

This is why self appears automatically in normal method calls.


Common Mistake: Calling a Class Function Without an Instance

Example:

class User:
    def greet(self):
        return "hello"


User.greet()

This fails because self is missing.

Correct:

user = User()
user.greet()

or:

User.greet(user)

The method needs an instance.

Access through an instance binds the instance automatically.

Access through the class gives the raw function, so you must pass the instance yourself.


Common Mistake: Using hasattr() Without Realizing It Runs Lookup

Example:

hasattr(obj, "name")

This performs attribute lookup.

For simple objects, that is harmless.

But for objects with properties or custom attribute access, lookup can run code.

Example:

class Example:
    @property
    def value(self):
        print("checking value")
        return 10

Then:

hasattr(Example(), "value")

can print:

checking value

This is not a reason to avoid hasattr().

It is a reason to understand it.

It asks:

can this attribute be looked up without AttributeError?

not:

is there a simple key in __dict__?

Common Mistake: Overusing Dynamic Attributes

Dynamic attribute tools:

getattr()
setattr()
delattr()

are powerful.

But code like this can become hard to understand:

for key, value in data.items():
    setattr(obj, key, value)

Now object attributes depend on external data.

Maybe that is exactly right.

Maybe it is dangerous.

Ask:

Which attributes should this object have?
Are they known and documented?
Could unexpected keys overwrite important attributes?
Should this data stay in a dictionary instead?

Classes should make structure clearer.

Do not use dynamic attributes in a way that hides structure.


Design Guidance

When designing attributes and methods, ask:

What state belongs to each instance?
What data should be shared by the class?
Which attributes are public?
Which attributes are internal?
Which behavior belongs as a method?
Should callers read this as an attribute or call it as a method?
Could a class attribute accidentally be mutated by all instances?
Does assignment mean per-instance override or shared class change?

Basic rules:

  • Store per-object state on instances.
  • Store truly shared defaults or constants on classes.
  • Avoid mutable class attributes unless sharing is intentional.
  • Use methods for behavior.
  • Keep public attributes clear and stable.
  • Use leading underscores for internal implementation details.
  • Prefer explicit attributes over mysterious dynamic ones.

Good object design is mostly clear ownership of names and behavior.


Exercises

  1. Create:
class User:
    role = "member"

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

Create a user.

Print:

vars(user)
vars(User)

Which namespace contains name?

Which namespace contains role?


  1. Predict the output:
class User:
    role = "member"


ada = User()
grace = User()

ada.role = "admin"

print(ada.role)
print(grace.role)
print(User.role)

Explain using attribute lookup and assignment.


  1. Predict the output:
class Team:
    members = []


red = Team()
blue = Team()

red.members.append("Ada")

print(blue.members)

Why does this happen?

Rewrite the class so each team has its own members list.


  1. Create a class with a method:
class Greeter:
    def greet(self):
        return "hello"

Inspect:

print(Greeter.greet)
print(Greeter().greet)

What is the difference?


  1. Show that these are equivalent:
obj.method()
Class.method(obj)

using a small class of your own.


  1. Use getattr() to read an attribute whose name is stored in a variable:
field = "name"

Then use setattr() to set another attribute dynamically.

When is this useful?

When might it be dangerous?


  1. Explain the difference between:
obj.attr = value

and:

obj.attr.append(value)

Use a class attribute list example.


  1. Create an instance attribute that shadows a class attribute.

Then delete the instance attribute.

Show that the class attribute becomes visible again.


  1. Explain why this fails:
class User:
    def greet(self):
        return "hello"


User.greet()

How can you call it correctly?


  1. In your own words, explain:
methods are attributes found through lookup

Use the terms:

class namespace
function object
bound method
self

Summary

In this chapter we learned:

  • Attributes are names attached to objects.
  • Dot syntax performs attribute access.
  • Instance attributes live on individual instances.
  • Class attributes live on class objects.
  • Ordinary instances often store attributes in __dict__.
  • Class objects have namespaces too.
  • Attribute lookup can find names on instances and classes.
  • Attribute assignment usually writes to the instance.
  • Instance attributes can shadow class attributes.
  • Deleting an instance attribute can reveal a class attribute.
  • Mutating a found class attribute can affect all instances.
  • Methods are functions stored on classes.
  • Accessing a method through an instance creates a bound method.
  • A bound method remembers the function and the instance.
  • obj.method() usually behaves like Class.method(obj).
  • getattr(), setattr(), hasattr(), and delattr() provide dynamic attribute access tools.
  • Attribute lookup prepares us for inheritance, properties, descriptors, and MRO.

Core model:

obj.attr lookup:
    look on object
    look on class
    look on parent classes
    maybe invoke descriptor behavior

obj.attr = value:
    usually assign on object

Method model:

class namespace:
    method_name -> function object

instance lookup:
    instance.method_name -> bound method

bound method:
    function + instance

Attribute lookup is the engine under object-oriented Python.

Once you understand it, the advanced topics become far less mysterious.


Preview of Chapter 45

Next we study encapsulation and managed attributes.

So far, attributes have mostly been direct:

user.name
user.email
account.balance

But real programs often need rules around attributes.

Chapter 45 explains how Python handles that without abandoning its object model.

We will study:

  • What encapsulation means in Python.
  • Public and internal attributes.
  • Leading underscore conventions.
  • Name mangling with double underscores.
  • Why Python does not use strict private fields by default.
  • How properties manage attribute access.
  • How validation can happen during assignment.
  • Why methods and properties are API design tools.
  • How managed attributes prepare us for descriptors.

The transition is direct:

attribute lookup explains how attributes are found
managed attributes explain how attribute access can be controlled

This is where Python's practical approach to encapsulation begins.