Session 27 - classmethod, staticmethod & property

Session 27 of the Python Fundamentals series, covered in depth. It covers three decorators that reshape how a class exposes behavior: @staticmethod, for a namespaced helper that takes no self; @classmethod, for cls-based alternate constructors such as from_string; and @property, which exposes a computed value that reads like a plain attribute, together with a matching setter that validates every assignment. The traps are assigning to a property that has no setter, which raises AttributeError: property '...' has no setter; forgetting @staticmethod and getting a positional-argument TypeError; and putting validation in a raw attribute, where nothing guards it. Every snippet and error message was executed and copied verbatim from CPython 3.12.

Subject: Python Fundamentals · 103 slides · code lesson

Open the interactive version of this deck · Homework for this lesson

What this lesson covers

The lesson, slide by slide

1. classmethod, staticmethod & property

Title

Python Fundamentals - Session 27

Three decorators that reshape how a class exposes behavior

2. What you will be able to do

Objectives

You already write classes with __init__ and regular methods that take self. This session adds three decorated methods. By the end you can:

  1. Write a @staticmethod - a helper in the class's namespace that takes no self.
  2. Write a @classmethod and use it as an alternate constructor like from_string.
  3. Expose a computed value with @property so obj.area runs code.
  1. Add a matching setter that validates every assignment to that property.
  2. Make a read-only property, and explain the no-setter AttributeError.
  3. Choose between a raw attribute and a property + setter for validation.

3. What survived from Session 26 - Inheritance & Composition?

Warm-up

Discussion prompt

Before we open Session 27 - classmethod, staticmethod & property: without looking back, what was the main idea of Session 26 - Inheritance & Composition, and what could you do by the end of it that you could not do before?

Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.

Answer:

Session 26 of the Python Fundamentals series, in depth. Two ways to reuse classes: inheritance (a Dog IS-A Animal - subclassing, overriding methods, and super() to reuse the parent) and composition (a Car HAS-A Engine - building objects out of other objects).

4. @staticmethod: A Namespaced Helper

Section

Part 1

5. Regular methods receive self

Concept

Every method you have written so far takes self as its first parameter - the instance the method was called on.

Sometimes a function belongs near a class but needs no instance at all. That is what @staticmethod is for.

6. Break it if you can: Regular methods receive self

Counterexample

Discussion prompt

Every method you have written so far takes self as its first parameter - the instance the method was called on.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

Answer:

Sometimes a function belongs near a class but needs no instance at all. That is what @staticmethod is for.

7. The @ line is a decorator

Concept

The @name line sitting directly above a def is a decorator - it wraps the method to change how it behaves. You have used them without naming them; here they are the whole point.

@staticmethod, @classmethod, and @property are all built in - no import needed.

8. By analogy: The @ line is a decorator

Analogy

Discussion prompt

Explain The @ line is a decorator by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

@staticmethod, @classmethod, and @property are all built in - no import needed.

9. @staticmethod takes no self

Concept

Put @staticmethod on the line above a method and drop self. It becomes a plain function that lives inside the class.

static method — A method with no self and no cls, marked @staticmethod. It cannot see any instance or class data - it just takes its arguments and returns a result.

10. What has to happen first: A Celsius-to-Fahrenheit helper

Ranking

Put in order

Put the moves of A Celsius-to-Fahrenheit helper into the order they have to happen.

  1. The @staticmethod line marks c_to_f
  2. Call it on the class name
  3. Read the output

Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. It has no self - it takes just a number c and returns a number.

11. A Celsius-to-Fahrenheit helper

Worked example

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    @staticmethod
    def c_to_f(c):
        return c * 9 / 5 + 32

print(Temperature.c_to_f(100))
print(Temperature.c_to_f(0))

The @staticmethod line marks c_to_f

Why: It has no self - it takes just a number c and returns a number.

Call it on the class name

Why: You do not need a Temperature instance to convert a number.

Read the output

Why: Verified by execution: 212.0 then 32.0.

callcreturns
Temperature.c_to_f(100)100212.0
Temperature.c_to_f(0)032.0

12. Fill in: c for A Celsius-to-Fahrenheit helper

Comparison

Comparison matrix

From A Celsius-to-Fahrenheit helper: refill the c column from what you know. The rest of the table is as it appeared.

callcreturns
Temperature.c_to_f(100)100212.0
Temperature.c_to_f(0)032.0

13. Call it on the class or an instance

Concept

A static method works both ways: Temperature.c_to_f(100) or t.c_to_f(100). Because it takes no self, the instance is ignored either way.

14. Teach it back: Call it on the class or an instance

Explain it

Discussion prompt

Explain Call it on the class or an instance to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.

Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.

Answer:

A static method works both ways: Temperature.c_to_f(100) or t.c_to_f(100). Because it takes no self, the instance is ignored either way.

15. Plan first: Static works from both

Step zero

Discussion prompt

Static works from both — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.

Hint: It starts with: Call on the class

Answer:

  1. Call on the class
  2. Call on an instance
  3. Read the output

16. Static works from both

Worked example

class MathUtil:
    @staticmethod
    def add(a, b):
        return a + b

print(MathUtil.add(2, 3))
u = MathUtil()
print(u.add(2, 3))

Call on the class

Why: MathUtil.add(2, 3) runs the helper directly.

Call on an instance

Why: u.add(2, 3) works too - the instance u is not passed in, because there is no self.

Read the output

Why: Verified by execution: 5 then 5 - the same answer both ways.

callreturns
MathUtil.add(2, 3)5
u.add(2, 3)5

17. What each one costs: Static works from both

Trade off

Comparison matrix

From Static works from both: every row here is a choice with a cost. Fill the returns column, then say which row you would actually pick and what you give up for it.

callreturns
MathUtil.add(2, 3)5
u.add(2, 3)5

18. A plain function that lives in the class

Intuition

Think of a static method as an ordinary function you filed inside the class for tidiness - grouped with the data it relates to.

It cannot touch any instance's attributes. Give it inputs, it gives you a result. Nothing more.

19. @classmethod: Alternate Constructors

Section

Part 2

20. @classmethod receives cls

Concept

Mark a method with @classmethod and its first parameter is cls - the class itself, not an instance.

class method — A method marked @classmethod whose first parameter cls is the class. It can read class-level data and, most usefully, build and return a new instance with cls(...).

21. Take the definitions apart: static method vs class method

Definition probe

Sort into buckets

Every line below is part of the definition of static method or of class method — one or the other, never both. Put each where it belongs.

static method
A method with no self and no cls, marked @staticmethod.; It cannot see any instance or class data - it just takes its arguments and returns a result.
class method
A method marked @classmethod whose first parameter cls is the class.; It can read class-level data and, most usefully, build and return a new instance with cls(...).
b1
A method with no self and no cls, marked @staticmethod. It cannot see any instance or class data - it just takes its arguments and returns a result.
b2
A method marked @classmethod whose first parameter cls is the class. It can read class-level data and, most usefully, build and return a new instance with cls(...).

22. Predict the next row: Count instances with cls

Pattern

Predict first

The table runs: start | 0 · Student("A") | 1 · Student("B") | 2

In Count instances with cls, given the rows so far: what is the next one — the row where event is how_many()?

Correct: how_many() | 2

eventcount
start0
Student("A")1
Student("B")2
how_many()2

Why: The relationship between the columns, not the individual numbers, is what generates the next row. It belongs to the class, shared by all instances - each __init__ bumps it.

23. Count instances with cls

Worked example

class Student:
    count = 0
    def __init__(self, name):
        self.name = name
        Student.count += 1

    @classmethod
    def how_many(cls):
        return cls.count

Student("A")
Student("B")
print(Student.how_many())

count is a class-level attribute

Why: It belongs to the class, shared by all instances - each __init__ bumps it.

how_many reads cls.count

Why: cls is the class, so cls.count is the shared counter.

Read the output

Why: Verified by execution: after two Students, prints 2.

eventcount
start0
Student("A")1
Student("B")2
how_many()2

24. Which is which, by count

Discrimination

Sort into buckets

Sort these by count, from memory, without looking back at Count instances with cls. Telling them apart on the spot is the skill; the table is only where the answer happens to be written down.

0
start
1
Student("A")
2
Student("B"); how_many()
g1
count is "0" for start — that is what the table on "Count instances with cls" records, and it is the single property separating this group from the rest.
g2
count is "1" for Student("A") — that is what the table on "Count instances with cls" records, and it is the single property separating this group from the rest.
g3
count is "2" for Student("B"), how_many() — that is what the table on "Count instances with cls" records, and it is the single property separating this group from the rest.

25. Alternate constructors

Concept

The best use of @classmethod is a second way to build an object. __init__ takes the normal arguments; a class method can build one from a different input.

By convention these are named from_something. Inside, they call cls(...) to make and return the new instance.

26. Restore the missing line: Student.from_string

Fill the middle

Fill in the blanks

From Student.from_string — one line has had its right-hand side removed. Put it back.

class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade

@classmethod
def from_string(cls, text):
name, grade = text.split(",")
return cls(name, int(grade))

s = Student.from_string("Ana,90")
print(s.name)
print(s.grade)

Why: self.grade is what everything below it consumes, so the wrong expression here fails later and somewhere else. text.split(",") gives ["Ana", "90"], unpacked into name and grade.

27. Student.from_string

Worked example

class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

    @classmethod
    def from_string(cls, text):
        name, grade = text.split(",")
        return cls(name, int(grade))

s = Student.from_string("Ana,90")
print(s.name)
print(s.grade)

Split the raw text

Why: text.split(",") gives ["Ana", "90"], unpacked into name and grade.

cls(name, int(grade)) builds the object

Why: cls is Student, so this runs __init__ and returns a real Student.

Read the output

Why: Verified by execution: Ana then 90 - grade is an int, not the string "90".

stepvalue
text"Ana,90"
name"Ana"
int(grade)90
s.name / s.gradeAna / 90

28. Inspect it line by line: Student.from_string

Error analysis

Annotate

Walk the callouts on Student.from_string. Each one is a place this is easy to get subtly wrong.

  • text.split(",") gives ["Ana", "90"], unpacked into name and grade.
  • cls is Student, so this runs __init__ and returns a real Student.
  • Verified by execution: Ana then 90 - grade is an int, not the string "90".

29. Plan first: Money.from_dollars

Step zero

Discussion prompt

Money.from_dollars — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.

Hint: It starts with: __init__ stores cents

Answer:

  1. __init__ stores cents
  2. from_dollars converts then builds
  3. Read the output

30. Money.from_dollars

Worked example

class Money:
    def __init__(self, cents):
        self.cents = cents

    @classmethod
    def from_dollars(cls, dollars):
        return cls(round(dollars * 100))

m = Money.from_dollars(4.50)
print(m.cents)

__init__ stores cents

Why: The object's real state is an integer count of cents.

from_dollars converts then builds

Why: 4.50 dollars -> round(450.0) -> 450 cents, wrapped by cls(...).

Read the output

Why: Verified by execution: 450. Two ways to build the same class - by cents or by dollars.

calldollarscents
Money(450)-450
Money.from_dollars(4.50)4.50450

31. Draw the shape of it: Money.from_dollars

Blank canvas

Draw it

Draw what Money.from_dollars just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.

32. Date.from_string

Worked example

class Date:
    def __init__(self, y, m, d):
        self.y, self.m, self.d = y, m, d

    @classmethod
    def from_string(cls, s):
        y, m, d = s.split("-")
        return cls(int(y), int(m), int(d))

    def __repr__(self):
        return f"Date({self.y}, {self.m}, {self.d})"

print(Date.from_string("2026-07-15"))

Split on the dashes

Why: "2026-07-15".split("-") gives three string pieces.

Convert each to int and build

Why: cls(int(y), int(m), int(d)) makes a Date with numeric fields.

Read the output

Why: Verified by execution: Date(2026, 7, 15).

pieceint
"2026"2026
"07"7
"15"15

33. Watch it run: Date.from_string

Pattern

Step through it

Step through Date.from_string one row at a time. What is driving the change, and what would the row after the last one be?

  1. Step 1: piece is "2026"
  2. Step 2: piece is "07"
  3. Step 3: piece is "15"

34. Why cls, not the class name

Concept

Inside a class method you could type Student(...), but cls(...) is better: if someone later subclasses Student, cls is that subclass, so from_string builds the right type automatically.

35. cls is 'whatever class this is'

Intuition

self was 'the object I was called on'. cls is the parallel idea one level up: 'the class I was called on'.

So cls(...) means 'make one of me' without hard-coding a name - a factory that always produces the current class.

36. Three Kinds of Method

Section

Part 3

37. Instance, class, and static

Concept

A class can hold all three. The decorator (or lack of one) decides what implicit first argument the method receives.

38. Who receives what

Concept

The difference is entirely in that first parameter: an instance, the class, or nothing.

decoratorfirst parametercan see
(none)selfthis instance's data
@classmethodclsthe class and its class-level data
@staticmethod(none)only its own arguments

39. Where does each piece belong: Session 27 - classmethod, staticmethod &…

Sorting

Sort into buckets

These are the pieces of Session 27 - classmethod, staticmethod & property, out of order. Put each one back under the part of the lesson it belongs to.

@staticmethod: A Namespaced Helper
Regular methods receive self; The @ line is a decorator; @staticmethod takes no self
@classmethod: Alternate Constructors
@classmethod receives cls; Count instances with cls; Alternate constructors
Three Kinds of Method
Instance, class, and static; Who receives what
s1
@staticmethod: A Namespaced Helper is where Session 27 - classmethod, staticmethod & property puts Regular methods receive self, The @ line is a decorator, @staticmethod takes no self. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s2
@classmethod: Alternate Constructors is where Session 27 - classmethod, staticmethod & property puts @classmethod receives cls, Count instances with cls, Alternate constructors. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s3
Three Kinds of Method is where Session 27 - classmethod, staticmethod & property puts Instance, class, and static, Who receives what. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.

40. @property: A Computed Attribute

Section

Part 4

41. property exposes a computed value

Concept

A @property lets a method be read like an attribute - no parentheses. obj.area runs the method and gives back its result.

property — A method marked @property that you access as if it were a plain attribute. Reading obj.name runs the method's body and returns its value.

42. Term to definition: Session 27 - classmethod, staticmethod & property

Matching

Match the pairs

Match each term to the definition this lesson gave it — not the one you would guess from the word.

  • t1. static method
  • t2. class method
  • t3. property
  • d1. A method with no self and no cls, marked @staticmethod. It cannot see any instance or class data - it just takes its arguments and returns a result.
  • d2. A method marked @classmethod whose first parameter cls is the class. It can read class-level data and, most usefully, build and return a new instance with cls(...).
  • d3. A method marked @property that you access as if it were a plain attribute. Reading obj.name runs the method's body and returns its value.

Why: These are the working definitions of static method, class method, property as Session 27 - classmethod, staticmethod & property uses them. Pairing them correctly is the test of whether you could state each one with the slide switched off.

43. What has to happen first: Circle.area as a property

Ranking

Put in order

Put the moves of Circle.area as a property into the order they have to happen.

  1. area is decorated with @property
  2. Change radius, read area again
  3. Read the output

Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. So c.area (no parentheses) runs the body and returns the number.

44. Circle.area as a property

Worked example

class Circle:
    def __init__(self, radius):
        self.radius = radius

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

c = Circle(10)
print(c.area)
c.radius = 2
print(c.area)

area is decorated with @property

Why: So c.area (no parentheses) runs the body and returns the number.

Change radius, read area again

Why: area is computed fresh each read, so it tracks the current radius.

Read the output

Why: Verified by execution: 314.159 then 12.56636.

radiusc.area
10314.159
212.56636

45. No parentheses to read it

Concept

That is the whole point: to the caller, c.area looks and feels like stored data, even though a calculation runs behind it.

So you can turn a plain attribute into a computed one later without changing any code that reads it.

46. Plan first: A property tracks its backing data

Step zero

Discussion prompt

A property tracks its backing data — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.

Hint: It starts with: area multiplies the two backing fields

Answer:

  1. area multiplies the two backing fields
  2. Change _w, read area again
  3. Read the output

47. A property tracks its backing data

Worked example

class Box:
    def __init__(self, w, h):
        self._w = w
        self._h = h

    @property
    def area(self):
        return self._w * self._h

b = Box(3, 4)
print(b.area)
b._w = 5
print(b.area)

area multiplies the two backing fields

Why: _w and _h hold the real data; area derives from them.

Change _w, read area again

Why: Nothing is cached - the next read recomputes with the new _w.

Read the output

Why: Verified by execution: 12 then 20.

_w_hb.area
3412
5420

48. Attribute on the outside, method on the inside

Intuition

A property is a disguise. Callers see a simple attribute; inside, a method decides what that attribute means.

That disguise is what lets you add logic - computing, checking, converting - without asking every caller to switch from obj.x to obj.x().

49. When to reach for a property

Concept

Start with a plain attribute. Upgrade to a @property only when a read needs to compute something, or a write needs to be checked - the property is the place to put that logic.

The win: callers keep writing obj.x, unaware the rules changed underneath them.

50. The Setter: Validating Assignment

Section

Part 5

51. A matching setter runs on assignment

Concept

Add a second method decorated with @name.setter and it runs whenever someone does obj.name = value. The assigned value arrives as a parameter.

Now the equals sign is not just storing data - it is running your code.

52. State the rule before it runs: celsius getter and setter

Hypothesis

Predict first

celsius getter and setter is about to be worked. State your hypothesis first: which rule or definition decides this one, and what is the first move it forces? Then watch whether the example agrees with you.

Correct: Getter returns the backing _celsius

Why: Reading t.celsius runs this and hands back the stored number.

A hypothesis you wrote down is falsifiable; a vague sense of how it will go is not. If the example opens somewhere else, that gap is the thing worth chasing.

53. celsius getter and setter

Worked example

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("below absolute zero")
        self._celsius = value

t = Temperature(20)
print(t.celsius)
t.celsius = 30
print(t.celsius)

Getter returns the backing _celsius

Why: Reading t.celsius runs this and hands back the stored number.

Setter guards, then stores

Why: t.celsius = 30 calls the setter with value=30; it passes the check and stores it.

Read the output

Why: Verified by execution: 20 then 30.

actionvaluet.celsius
Temperature(20)2020
read-20
t.celsius = 303030

54. Fill in: t.celsius for celsius getter and setter

Comparison

Comparison matrix

From celsius getter and setter: refill the t.celsius column from what you know. The rest of the table is as it appeared.

actionvaluet.celsius
Temperature(20)2020
read-20
t.celsius = 303030

55. Put the validation in the setter

Concept

Because the setter runs on every assignment, it is the one place to reject bad values - raise an error and the assignment never happens.

56. Restore the missing line: The setter rejects a bad value

Fill the middle

Fill in the blanks

From The setter rejects a bad value — one line has had its right-hand side removed. Put it back.

Traceback (most recent call last):
File "temp.py", line 16, in <module>
t.celsius = -300
^^^^^^^^^
File "temp.py", line 12, in celsius
raise ValueError("below absolute zero")
ValueError: below absolute zero

Why: t.celsius is what everything below it consumes, so the wrong expression here fails later and somewhere else. The setter's check is True, so it raises instead of storing.

57. The setter rejects a bad value

Worked example

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("below absolute zero")
        self._celsius = value

t = Temperature(20)
t.celsius = -300

-300 is below absolute zero

Why: The setter's check is True, so it raises instead of storing.

The assignment is refused

Why: Verified by execution: the program stops with the traceback below - _celsius stays 20.

Traceback (most recent call last):
  File "temp.py", line 16, in <module>
    t.celsius = -300
    ^^^^^^^^^
  File "temp.py", line 12, in celsius
    raise ValueError("below absolute zero")
ValueError: below absolute zero
assignmentcheckresult
t.celsius = 3030 < -273.15 is Falsestored
t.celsius = -300-300 < -273.15 is TrueValueError

58. The setter runs even from __init__

Concept

If __init__ assigns self.value = value and value is a property, that assignment goes through the setter too - so the guard protects construction, not just later edits.

59. Predict the next row: Building through the setter

Pattern

Predict first

The table runs: Score(85) | 85 | 85 · read | - | 85

In Building through the setter, given the rows so far: what is the next one — the row where action is s.value = 92?

Correct: s.value = 92 | 92 | 92

actionvs.value
Score(85)8585
read-85
s.value = 929292

Why: The relationship between the columns, not the individual numbers, is what generates the next row. value is a property, so this runs the setter - Score(85) is validated on the way in.

60. Building through the setter

Worked example

class Score:
    def __init__(self, value):
        self.value = value

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, v):
        if not 0 <= v <= 100:
            raise ValueError("score must be 0-100")
        self._value = v

s = Score(85)
print(s.value)
s.value = 92
print(s.value)

__init__ assigns self.value

Why: value is a property, so this runs the setter - Score(85) is validated on the way in.

Both reads and later writes flow through

Why: s.value reads via the getter; s.value = 92 writes via the setter.

Read the output

Why: Verified by execution: 85 then 92. Score(150) would raise ValueError: score must be 0-100.

actionvs.value
Score(85)8585
read-85
s.value = 929292

61. Watch it run: Building through the setter

Pattern

Step through it

Step through Building through the setter one row at a time. What is driving the change, and what would the row after the last one be?

  1. Step 1: action is Score(85)
  2. Step 2: action is read
  3. Step 3: action is s.value = 92

62. Something is wrong here: a raw attribute guards nothing

Anomaly

Predict first

A student writes this, and it looks reasonable:

You meant to keep the value valid, but stored it as a plain attribute.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: self.celsius is just data; = stores -300 with no guard, even though it is impossible.

Make celsius a property with a setter that validates.

Why: self.celsius is just data; = stores -300 with no guard, even though it is impossible.

63. Trap: a raw attribute guards nothing

Trap

The trap

You meant to keep the value valid, but stored it as a plain attribute.

class Temp:
    def __init__(self, celsius):
        self.celsius = celsius

t = Temp(20)
t.celsius = -300
print(t.celsius)

Nothing checks the assignment

Why: self.celsius is just data; = stores -300 with no guard, even though it is impossible.

assignmentcheckt.celsius
t.celsius = -300none-300 (verified)

The fix

Make celsius a property with a setter that validates.

class Temp:
    def __init__(self, celsius):
        self.celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("below absolute zero")
        self._celsius = value

Every assignment runs the guard

Why: Now t.celsius = -300 raises ValueError: below absolute zero, so the bad value is never stored.

assignmentcheckresult
t.celsius = -300-300 < -273.15ValueError (blocked)

64. Read-Only Properties

Section

Part 6

65. A getter with no setter is read-only

Concept

Define @property but no matching setter, and the value can be read but not assigned. Trying to set it raises an AttributeError.

This is exactly what you want for a computed or protected value - area, balance, an id.

66. Restore the missing line: A read-only balance

Fill the middle

Fill in the blanks

From A read-only balance — one line has had its right-hand side removed. Put it back.

Traceback (most recent call last):
File "acct.py", line 11, in <module>
a.balance = 200
^^^^^^^^^
AttributeError: property 'balance' of 'Account' object has no setter

Why: a.balance is what everything below it consumes, so the wrong expression here fails later and somewhere else. Reading a.balance works; assigning to it has nothing to run.

67. A read-only balance

Worked example

class Account:
    def __init__(self, balance):
        self._balance = balance

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

a = Account(100)
print(a.balance)
a.balance = 200

balance has a getter but no setter

Why: Reading a.balance works; assigning to it has nothing to run.

Assigning fails

Why: Verified by execution: it prints 100, then the assignment raises the traceback below.

Traceback (most recent call last):
  File "acct.py", line 11, in <module>
    a.balance = 200
    ^^^^^^^^^
AttributeError: property 'balance' of 'Account' object has no setter
actionresult
print(a.balance)100
a.balance = 200AttributeError: ... has no setter

68. Derived read-only values

Concept

A read-only property can be computed from another. Store celsius; expose fahrenheit as a property with no setter, derived on every read.

69. Plan first: fahrenheit derived from celsius

Step zero

Discussion prompt

fahrenheit derived from celsius — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.

Hint: It starts with: celsius is the stored, settable value

Answer:

  1. celsius is the stored, settable value
  2. fahrenheit is computed and read-only
  3. Read the output

70. fahrenheit derived from celsius

Worked example

class Temperature:
    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        self._celsius = value

    @property
    def fahrenheit(self):
        return self._celsius * 9 / 5 + 32

t = Temperature()
t.celsius = 100
print(t.fahrenheit)
t.celsius = 0
print(t.fahrenheit)

celsius is the stored, settable value

Why: Its setter records _celsius.

fahrenheit is computed and read-only

Why: No setter - it is derived from _celsius each time you read it.

Read the output

Why: Verified by execution: 212.0 then 32.0.

t.celsiust.fahrenheit
100212.0
032.0

71. Draw the shape of it: fahrenheit derived from celsius

Blank canvas

Draw it

Draw what fahrenheit derived from celsius just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.

72. Something is wrong here: setting a property with no setter

Anomaly

Predict first

A student writes this, and it looks reasonable:

You expose a computed value, then try to assign to it.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: area is computed from radius; there is no method to run for an assignment, so Python raises.

Set the input, not the computed output - change radius and let area follow.

Why: area is computed from radius; there is no method to run for an assignment, so Python raises.

73. Trap: setting a property with no setter

Trap

The trap

You expose a computed value, then try to assign to it.

class Circle:
    def __init__(self, radius):
        self.radius = radius

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

c = Circle(10)
c.area = 50

area has no setter

Why: area is computed from radius; there is no method to run for an assignment, so Python raises.

Traceback (most recent call last):
  File "circle.py", line 10, in <module>
    c.area = 50
    ^^^^^^
AttributeError: property 'area' of 'Circle' object has no setter
you writeresult
c.area = 50AttributeError: ... has no setter

The fix

Set the input, not the computed output - change radius and let area follow.

class Circle:
    def __init__(self, radius):
        self.radius = radius

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

c = Circle(10)
c.radius = 4
print(c.area)

Assign radius; read area

Why: Verified by execution: c.area is 50.26544. area is derived, so you steer it through its input.

c.radiusc.area
10314.159
450.26544

74. Break it on purpose: setting a property with no setter

Break the constraint

Discussion prompt

The rule this trap just fixed:

Verified by execution: c.area is 50.26544. area is derived, so you steer it through its input.

Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?

Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.

Answer:

area is computed from radius; there is no method to run for an assignment, so Python raises.

75. Pitfalls & Combining

Section

Part 7

76. Something is wrong here: forgetting @staticmethod

Anomaly

Predict first

A student writes this, and it looks reasonable:

A helper that takes no instance data, but you left off @staticmethod.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: u.add(2, 3) secretly passes u as the first argument, so add gets three values for two parameters.

Mark it @staticmethod so no instance is passed.

Why: u.add(2, 3) secretly passes u as the first argument, so add gets three values for two parameters.

77. Trap: forgetting @staticmethod

Trap

The trap

A helper that takes no instance data, but you left off @staticmethod.

class MathUtil:
    def add(a, b):
        return a + b

print(MathUtil.add(2, 3))
u = MathUtil()
print(u.add(2, 3))

On an instance it breaks

Why: u.add(2, 3) secretly passes u as the first argument, so add gets three values for two parameters.

Traceback (most recent call last):
  File "m.py", line 7, in <module>
    print(u.add(2, 3))
          ^^^^^^^^^^^
TypeError: MathUtil.add() takes 2 positional arguments but 3 were given
callresult
MathUtil.add(2, 3)5 (works by luck)
u.add(2, 3)TypeError: ... 3 were given

The fix

Mark it @staticmethod so no instance is passed.

class MathUtil:
    @staticmethod
    def add(a, b):
        return a + b

print(MathUtil.add(2, 3))
u = MathUtil()
print(u.add(2, 3))

Both calls work

Why: Verified by execution: 5 then 5. With @staticmethod, u is not passed in, so add gets exactly a and b.

callreturns
MathUtil.add(2, 3)5
u.add(2, 3)5

78. Which of these survive contact with Session 27 - classmethod, staticmethod &…?

Two truths and a lie

Sort into buckets

Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.

Holds up
Every method you have written so far takes self as its first parameter - the instance the method was called on.; @staticmethod, @classmethod, and @property are all built in - no import needed.; A static method works both ways: Temperature.c_to_f(100) or t.c_to_f(100). Because it takes no self, the instance is ignored either way.
Breaks
You meant to keep the value valid, but stored it as a plain attribute.; You expose a computed value, then try to assign to it.
sound
These are stated as this lesson states them — each one survives the edge cases Session 27 - classmethod, staticmethod & property puts it through.
flawed
Each of these is lifted from a trap in this deck: reasonable-sounding, and wrong in a way that only shows up once you rely on it.

79. The underscore backing field

Concept

When a property named celsius needs somewhere to store its data, the convention is a single leading underscore: _celsius. The property is the public door; _celsius is the private room behind it.

The underscore is a signal to other programmers - 'internal, do not touch directly' - not an enforced lock.

80. Teach it back: The underscore backing field

Explain it

Discussion prompt

Explain The underscore backing field to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.

Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.

Answer:

When a property named celsius needs somewhere to store its data, the convention is a single leading underscore: _celsius. The property is the public door; _celsius is the private room behind it.

81. Do not name the property and its field the same

Concept

The getter must return self._celsius, not self.celsius. Returning self.celsius would read the property again, which reads the property again - endless recursion until Python stops with a RecursionError.

The underscore field breaks that loop: the property is the public door, _celsius is the actual storage behind it.

82. By analogy: Do not name the property and its field the same

Analogy

Discussion prompt

Explain Do not name the property and its field the same by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

The underscore field breaks that loop: the property is the public door, _celsius is the actual storage behind it.

83. The decorators combine

Concept

A @property can call a @staticmethod for its computation, keeping the formula in one reusable place.

84. Break it if you can: The decorators combine

Counterexample

Discussion prompt

A @property can call a @staticmethod for its computation, keeping the formula in one reusable place.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

85. What has to happen first: A property backed by a static helper

Ranking

Put in order

Put the moves of A property backed by a static helper into the order they have to happen.

  1. with_tax is a static formula
  2. total reads like an attribute
  3. Read the output

Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. It takes a number and adds 8% - usable with or without an instance.

86. A property backed by a static helper

Worked example

class Price:
    def __init__(self, amount):
        self.amount = amount

    @staticmethod
    def with_tax(amount):
        return amount + amount * 0.08

    @property
    def total(self):
        return Price.with_tax(self.amount)

p = Price(100)
print(p.total)
print(Price.with_tax(50))

with_tax is a static formula

Why: It takes a number and adds 8% - usable with or without an instance.

total reads like an attribute

Why: p.total runs the property, which calls the static helper on the instance's amount.

Read the output

Why: Verified by execution: 108.0 then 54.0.

callamountresult
p.total100108.0
Price.with_tax(50)5054.0

87. Inspect it line by line: A property backed by a static helper

Error analysis

Annotate

Walk the callouts on A property backed by a static helper. Each one is a place this is easy to get subtly wrong.

  • It takes a number and adds 8% - usable with or without an instance.
  • p.total runs the property, which calls the static helper on the instance's amount.

88. Patterns & Checks

Section

Part 8

89. Choosing the decorator

Pattern

Needs this instance's data? plain method with self

Why: The default - it reads or changes attributes on self.

Builds an instance or reads class data? @classmethod with cls

Why: Alternate constructors (from_string) and class-wide counters live here.

Needs neither self nor cls? @staticmethod

Why: A pure helper filed inside the class for tidiness.

Should read like an attribute? @property

Why: Expose a computed value as obj.name, no parentheses.

90. A validated property

Pattern

1. Store the real data in _name

Why: The single leading underscore marks it as the private backing field.

2. @property def name(self): return self._name

Why: The public read - callers use obj.name with no parentheses.

3. @name.setter def name(self, value): check, then store

Why: Raise on a bad value; otherwise set self._name = value.

4. Assign self.name in __init__ so construction is validated too

Why: The setter runs on that assignment, so no object is ever built invalid.

91. An alternate constructor

Pattern

1. @classmethod def from_x(cls, raw):

Why: Name it from_ the kind of input it accepts.

2. Parse raw into the pieces __init__ needs

Why: Split a string, convert types, pull fields apart.

3. return cls(...) with those pieces

Why: cls(...) runs __init__ and returns the new instance - subclass-friendly.

92. Where this shows up: Session 27 - classmethod, staticmethod & property

Real world

Discussion prompt

Outside this lesson: where does Session 27 - classmethod, staticmethod & property actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of An alternate constructor is doing the work in it.

Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.

Answer:

Session 27 of the Python Fundamentals series, in depth. Three decorators that reshape how a class exposes behavior: @staticmethod for a namespaced helper that takes no self, @classmethod for cls-based alternate constructors like from_string, and @property to expose a computed value that reads like a plain attribute - plus a matching setter that validates every assignment.

93. Check: static call

Check

No instance is made - trace the call.

class Util:
    @staticmethod
    def triple(n):
        return n * 3

print(Util.triple(4))
callnreturns
Util.triple(4)4?

Check your understanding

What does this print?

  • A. 12 (correct)
  • B. None
  • C. TypeError - missing self
  • D. <function triple>

Answer: A

Why: triple is a @staticmethod, so it takes only n. Util.triple(4) returns 4 * 3 = 12. Verified by execution.

Why B tempts people
There is a return, so the result is 12, not None.
Why C tempts people
A static method takes no self, so calling it on the class with one argument is correct - no error.
Why D tempts people
The parentheses call the method; without them you would get the function object, but here it is called.

94. Check: what from_string returns

Check

cls is the class itself.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    @classmethod
    def from_string(cls, s):
        x, y = s.split(",")
        return cls(int(x), int(y))

p = Point.from_string("3,4")
print(p.x)
sxp.x
"3,4""3"?

Check your understanding

What does print(p.x) show?

  • A. 3 (correct)
  • B. "3"
  • C. None
  • D. cls

Answer: A

Why: from_string splits "3,4", converts with int, and returns cls(3, 4) - a real Point whose x is the integer 3. Verified by execution.

Why B tempts people
int(x) converts the string "3" to the integer 3, so p.x prints as 3, not "3".
Why C tempts people
The method returns cls(...), so p is a real Point with x set - not None.
Why D tempts people
cls is the class Point inside the method; the code returns an instance, and p.x is its x value.

95. Check: reading a property

Check

Note there are no parentheses on total.

class Cart:
    def __init__(self, price, qty):
        self.price = price
        self.qty = qty
    @property
    def total(self):
        return self.price * self.qty

c = Cart(5, 3)
print(c.total)
priceqtyc.total
53?

Check your understanding

What does print(c.total) show?

  • A. 15 (correct)
  • B. <bound method> - total was not called
  • C. None
  • D. TypeError: total takes no arguments

Answer: A

Why: Because total is a @property, c.total runs the method and returns 5 * 3 = 15 - no parentheses needed. Verified by execution.

Why B tempts people
The @property means reading c.total runs the method; you do not get an uncalled method object.
Why C tempts people
The method returns price * qty, so the value is 15, not None.
Why D tempts people
A property is read as an attribute, so there is no call with arguments to mismatch.

96. Check: no setter

Check

area is a property with only a getter.

class Sq:
    def __init__(self, side):
        self.side = side
    @property
    def area(self):
        return self.side ** 2

s = Sq(3)
s.area = 100
lineresult
Sq(3)ok
s.area (read)9
s.area = 100?

Check your understanding

What happens on s.area = 100?

  • A. AttributeError: property 'area' of 'Sq' object has no setter (correct)
  • B. It sets area to 100 and overrides the getter
  • C. It silently does nothing
  • D. SyntaxError

Answer: A

Why: area is a property with no matching setter, so assigning to it raises AttributeError: property 'area' of 'Sq' object has no setter. Verified by execution.

Why B tempts people
With no setter defined, Python does not fall back to storing an attribute - it raises.
Why C tempts people
The assignment is not ignored; it raises an AttributeError and stops the program.
Why D tempts people
The syntax is valid Python; the error happens at runtime, not while parsing.

97. What each one costs: Check: no setter

Trade off

Comparison matrix

From Check: no setter: every row here is a choice with a cost. Fill the result column, then say which row you would actually pick and what you give up for it.

lineresult
Sq(3)ok
s.area (read)9
s.area = 100?

98. Check: setter validation

Check

The setter guards the value.

class Score:
    def __init__(self, v):
        self.v = v
    @property
    def v(self):
        return self._v
    @v.setter
    def v(self, value):
        if not 0 <= value <= 100:
            raise ValueError("score must be 0-100")
        self._v = value

s = Score(150)
stepvalue0<=v<=100
Score(150)150False

Check your understanding

What happens when Score(150) runs?

  • A. ValueError: score must be 0-100 (correct)
  • B. It builds a Score with v = 150
  • C. It builds a Score with v = 100
  • D. Nothing - __init__ skips the setter

Answer: A

Why: __init__ does self.v = v, which runs the setter with value=150; the check fails and it raises ValueError: score must be 0-100. Verified by execution.

Why B tempts people
The setter raises before storing, so no valid Score is built - construction fails.
Why C tempts people
The setter does not clamp the value; it raises on anything outside 0-100.
Why D tempts people
Assigning self.v in __init__ goes through the property setter just like any other assignment.

99. Check: cls counter

Check

count is shared at the class level.

class Dog:
    count = 0
    def __init__(self):
        Dog.count += 1
    @classmethod
    def total(cls):
        return cls.count

Dog()
Dog()
Dog()
print(Dog.total())
eventcount
start0
3 x Dog()?
total()?

Check your understanding

What does print(Dog.total()) show?

  • A. 3 (correct)
  • B. 0
  • C. 1
  • D. None

Answer: A

Why: Each Dog() bumps the shared Dog.count, so after three it is 3; total() returns cls.count = 3. Verified by execution.

Why B tempts people
count starts at 0 but each __init__ increments it, so after three Dogs it is 3.
Why C tempts people
The counter is class-level and shared, so it accumulates across all instances, not reset per Dog.
Why D tempts people
total returns cls.count, a number, so it is 3, not None.

100. Fill in: count for Check: cls counter

Comparison

Comparison matrix

From Check: cls counter: refill the count column from what you know. The rest of the table is as it appeared.

eventcount
start0
3 x Dog()?
total()?

101. Check: derived read-only

Check

fahrenheit has no setter.

class T:
    @property
    def celsius(self):
        return self._c
    @celsius.setter
    def celsius(self, v):
        self._c = v
    @property
    def fahrenheit(self):
        return self._c * 9 / 5 + 32

t = T()
t.celsius = 100
print(t.fahrenheit)
t.celsiust.fahrenheit
100?

Check your understanding

What does print(t.fahrenheit) show?

  • A. 212.0 (correct)
  • B. 100
  • C. AttributeError - fahrenheit has no setter
  • D. 180.0

Answer: A

Why: celsius = 100 stores _c = 100; reading fahrenheit computes 100 * 9 / 5 + 32 = 212.0. It is only read here, so the missing setter is fine. Verified by execution.

Why B tempts people
fahrenheit is a separate computed property, not celsius - it converts, giving 212.0.
Why C tempts people
No setter is needed because fahrenheit is only read, never assigned.
Why D tempts people
180.0 is 100 * 9 / 5 without the + 32; the formula adds 32 to reach 212.0.

102. Connect it up: Session 27 - classmethod, staticmethod & property

Connect it up

Draw it

One page, no notation unless you need it: draw how these connect — @staticmethod: A Namespaced Helper · @classmethod: Alternate Constructors · Three Kinds of Method · @property: A Computed Attribute · The Setter: Validating Assignment · Read-Only Properties. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.

103. What you can do now

Recap

Three decorators reshape a class: @staticmethod (no self), @classmethod (cls, for alternate constructors), and @property (a computed attribute, with an optional validating setter).

You writeIt means
@staticmethod def f(a):a helper with no self; call on class or instance
@classmethod def from_x(cls, s):build and return cls(...) from other input
@property def area(self):read obj.area like an attribute; runs the method
@area.setter def area(self, v):obj.area = v runs code - validate here
property, no setterread-only; assigning raises AttributeError

Reach for a property + setter when an attribute needs a guard; a raw attribute stores anything unchecked. Next session we build on this with dunder methods and operator overloading.

Sources

  1. Python 3 - @staticmethod
  2. Python 3 - @classmethod
  3. Python 3 - property
  4. All snippets and error messages executed and copied from CPython 3.12. — Author verification run, 2026-07-15 (Python Fundamentals series, Session 27).

Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108