Classes and Objects
Intermediate⏱75 minutes📖15 min readLesson 1 of 10

Classes and Objects

Build Python classes that hold state correctly: __init__ and __new__, instance versus class attributes, bound methods, repr and str, properties, slots, equality and hash, dataclasses and validated models

A support engineer opens the billing queue and finds every ticket carrying the tag billing. Only one customer ever used that tag. The code looks harmless: the Ticket class declares tags: list[str] = [] at class level, and each ticket calls self.tags.append(tag). Nothing in that line says "share this list with every other ticket", yet that is exactly what Python does.

This is the single most common bug in intermediate Python code. It is not a typo and it is not a subtle race. It is a consequence of how a class stores attributes, and once you can see the mechanism, the fix takes one line. The same mechanism explains bound methods, default values, properties, __slots__ and the equality rules that make objects safe as dictionary keys.

This lesson builds that mental model from the ground up, then turns it into working code. You will write a class that keeps its state private to each instance, give it readable string forms, enforce an invariant with a property, measure what a per-instance dictionary costs, and design a value object that behaves correctly as a dictionary key. The final section compares a hand-written class with a dataclass and a validated model, so you can pick the cheapest tool that meets the requirement.

Key terms

Class
A namespace that holds attributes and methods, created once when the class statement runs.
Instance
An object created by calling a class, with its own namespace for instance attributes.
Attribute
A name bound to a value on an object, looked up through the instance, its class and the base classes.
Method
A function defined in a class body that receives the instance as its first argument when called on an instance.
self
The conventional name for the first parameter of an instance method, bound to the instance at call time.
Dunder method
A special method with two leading and two trailing underscores, called by Python syntax such as print, == or len.
Class attribute
A value stored on the class object and shared by every instance that does not shadow it.
Instance attribute
A value stored in one instance namespace, independent of every other instance.
Property
A data descriptor that runs a getter and an optional setter when an attribute is read or assigned.
Name mangling
The rewrite of an identifier like __token to _ClassName__token inside a class body.
Dataclass
A class decorated with dataclasses.dataclass that generates __init__, __repr__ and __eq__ from annotated fields.
Sentinel
A unique object used as a placeholder when None is a valid value, created in Python 3.15 with sentinel().

1. What a Class Really Is

A class statement is executable code. Python runs the body once, in a fresh namespace, and binds that namespace to a class object. Functions become methods, plain assignments become class attributes, and calling the class later runs the two step creation protocol the data model defines.

The first step is __new__, which allocates and returns a new instance. The second is __init__, which receives that instance and fills it with state. __init__ is an initializer, not a constructor: it must return None, and a non-None return raises TypeError. Most classes never override __new__. Subclasses of immutable types such as int, str and tuple are the exception, because their value must be fixed first.

The class below models an invoice line. currency is deliberately a class attribute, because every invoice in this service bills in one currency. Everything else is instance state.

python
class Invoice: """One invoice line for one customer.""" currency = "USD" # class attribute: one value shared by all instances def __init__(self, customer: str, amount_cents: int) -> None: self.customer = customer # instance attribute self.amount_cents = amount_cents # instance attribute self.paid = False # instance attribute def total(self) -> str: dollars = self.amount_cents / 100 return f"{dollars:.2f} {self.currency}" def mark_paid(self) -> None: self.paid = True invoice = Invoice("Ada Lovelace", 129_900) print(invoice.total()) print(invoice.paid) invoice.mark_paid() print(invoice.paid)

The output is 1299.00 USD, then False, then True. Money stays an integer count of cents, and conversion happens only for display, which avoids the rounding surprises of binary floats.

The structure of this class fits in one diagram. The diagram separates shared class state from per-instance state and shows the containment relationship to a line item.

100%
Description

A class diagram of Invoice with a shared currency class field, four instance fields and three methods, plus a composition link to LineItem with one or many items.

The dollar suffix on currency marks a static member, which is the diagram's way of saying that the value lives on the class rather than on each instance. Everything else appears on every invoice.

The video below walks through the same creation sequence with a different example, so you can watch an instance being built and called.

Corey SchaferWatch on YouTube
Description

A video lesson that defines a simple class, creates instances from it, and calls methods on those instances to show where instance state lives.

ℹ️Note

Calling a class is not special syntax. Invoice("Ada", 129_900) calls the class object, which runs __new__ and then __init__. You never write new in Python.

2. Instance State and Class State

Every instance carries a dictionary named __dict__ unless the class uses __slots__. That dictionary is the instance namespace. The class also has a namespace, and attribute lookup walks from the instance to the class and then through the base classes. The trap starts when a class attribute is mutable.

The code below reproduces the opening bug in full. Read the output before reading the explanation.

python
class Ticket: tags: list[str] = [] # one list, created once, shared by all instances def __init__(self, subject: str) -> None: self.subject = subject def add_tag(self, tag: str) -> None: self.tags.append(tag) billing = Ticket("Refund stuck") billing.add_tag("billing") login = Ticket("Cannot sign in") print(login.tags) print(billing.tags is login.tags)

The output is ['billing'] followed by True. The second line is the proof. login.tags and billing.tags are not two equal lists. They are the same list object, because neither instance has a tags entry in its own namespace. The lookup finds the class attribute, and .append mutates it in place.

The fix is to create a fresh list for each instance inside __init__.

python
class Ticket: def __init__(self, subject: str) -> None: self.subject = subject self.tags: list[str] = [] # a new list for this instance only def add_tag(self, tag: str) -> None: self.tags.append(tag) billing = Ticket("Refund stuck") billing.add_tag("billing") login = Ticket("Cannot sign in") print(login.tags) print(billing.tags)

Now the output is [] and then ['billing']. Each instance owns its list.

There is a second version of the same bug. Assigning through an instance creates an instance attribute that shadows the class value. If a class declares total = 0, then self.total += 1 writes 1 into that instance, while every other instance still reads the class value.

The diagram below shows the order Python uses for a plain attribute read. The data descriptor branch is the reason a property can override an instance dictionary.

100%
Description

Attribute lookup starts with data descriptors on the class, then the instance dictionary, then the class and its base classes, and finally the __getattr__ hook or an AttributeError.

⚠️Warning

A mutable class attribute is shared state by default. Lists, dictionaries and sets declared in a class body belong in __init__ unless sharing is the explicit design. Function default arguments have the same trap.

The ordering exercise below checks the creation path, from the class statement to the first usable instance.

Ordering Question
How to answer

Ordering exercise with 5 items. Move them into the correct order, then check your answer.

Order the steps that happen when Python creates and initializes an instance.

The class statement runs and binds a class object
Calling the class invokes __new__
__new__ returns a fresh instance
__init__ receives the instance and fills its attributes
The initialized instance is returned to the caller

3. Methods and the Instance They Carry

A function on a class is a descriptor. Reading it through an instance calls the function's __get__ and returns a bound method that remembers the instance. Calling that method prepends the instance to the argument list, which is why self appears without being passed.

The sequence below follows one call from the caller to the function object.

100%
Description

A sequence diagram of invoice.total: the caller asks the instance, the instance looks up total on the class, the class returns the function, the function binds self, and the caller receives the formatted string.

The equivalence below is worth running once. The second call is legal, and it shows that self is an ordinary parameter.

python
invoice = Invoice("Grace Hopper", 250_000) print(invoice.total()) print(Invoice.total(invoice))

Both lines print 2500.00 USD. The second form calls the function through the class, so no binding happens and the instance must be passed explicitly.

💡Tip

Use self for the first parameter, even though the name is only a convention. Every Python reader expects it, linters check for it, and renaming it to this makes the class look like a failed port from Java.

4. Two String Representations

Python gives every object two string forms, and they answer different questions.

Method
Called by
Audience
Contract
__repr__
repr(), the REPL, debuggers
Developer
Should look like a valid expression that could recreate the object
__str__
print(), str(), f-strings
User
Should be readable and concise

print() asks for __str__ first. If a class defines __repr__ but not __str__, the data model falls back to __repr__ for both. That fallback is why many small classes only implement __repr__.

The class below stores money as integer cents, then provides both forms. The !r conversion inside the f-string applies repr() to each value, which keeps strings quoted and makes the output unambiguous.

python
class Money: def __init__(self, cents: int, currency: str = "USD") -> None: self.cents = cents self.currency = currency def __repr__(self) -> str: return f"Money(cents={self.cents!r}, currency={self.currency!r})" def __str__(self) -> str: return f"{self.cents / 100:.2f} {self.currency}" price = Money(4999) print(repr(price)) print(str(price)) print(price)

The output is Money(cents=4999, currency='USD') followed by 49.99 USD twice. The first line could be pasted back into a Python session. The other two are for people.

A default __repr__ prints the class name and a memory address that changes on every run. Replacing it is one of the highest-value lines you can add.

📌Important

Write __repr__ for every class you expect to debug. Add __str__ only when users see the object. A good __repr__ names the class and every field that matters, and it never performs I/O.

The matching exercise connects each special method to the syntax that calls it.

Matching Question
How to answer

Matching exercise with 6 pairs. Match each item on the left with its answer on the right.

Match each special method to the operation that calls it.

__repr__
Drop here
__str__
Drop here
__eq__
Drop here
__hash__
Drop here
__len__
Drop here
__bool__
Drop here

Drag answers from here:

repr() and the interactive prompt
print() and str()
The == operator
Dictionary keys and set members
The len() built-in
Truth testing in an if statement

5. Properties and Invariants

A public attribute is a promise. If every reader can assign radius_m = -3, the class has no invariant, and every method that computes with the radius has to defend itself. A property keeps the attribute syntax and adds a check.

A property is a data descriptor: it defines __get__ and, when a setter exists, __set__. Data descriptors take priority over the instance dictionary, which is why assigning self.radius_m inside __init__ still runs the setter. The built-in property() type is documented as a managed attribute, and the decorator form is the usual style.

A=πr2A = \pi r^2

The formula is why the radius must stay positive. A negative radius produces a positive area, so the bug passes a casual test and fails in production.

python
import math class Circle: def __init__(self, radius_m: float) -> None: self.radius_m = radius_m # goes through the setter below @property def radius_m(self) -> float: return self._radius_m @radius_m.setter def radius_m(self, value: float) -> None: if value <= 0: raise ValueError("radius must be positive") self._radius_m = value @property def area_m2(self) -> float: return math.pi * self._radius_m ** 2 circle = Circle(2.0) print(round(circle.area_m2, 2)) circle.radius_m = 3.0 print(round(circle.area_m2, 2))

The output is 12.57 and then 28.27, both rounded from the exact float. A call such as Circle(0) raises ValueError before any object escapes. The private storage name _radius_m is a convention, not a lock, but it tells readers to use the property.

ℹ️Note

A read-only property has a getter and no setter. Assigning to it raises AttributeError, which is the correct behaviour for a computed value such as area_m2.

The fill in the blank exercise checks the descriptor vocabulary.

Fill in the Blanks
How to answer

Fill-in-the-blank exercise with 4 blanks. Type the missing words, then check your answer.

Complete the description of a property.

Decorating a method with blank turns it into a data descriptor. The getter runs when you read the attribute, and the blank runs when you assign to it. Because the descriptor lives on the blank, it takes priority over the instance blank.

Word Bank, drag words into the blanks above:

property
setter
class
dictionary
classmethod
deleter
module
list

6. The Real Cost of a __dict__

Every normal instance carries a dictionary. The dictionary is flexible: you can add attributes at runtime, and each name is a hash lookup. That flexibility has a memory cost, and at a million objects the cost becomes the design constraint.

__slots__ replaces the per-instance dictionary with a fixed set of descriptors on the class. The data model states that this prevents the automatic creation of __dict__ and __weakref__ for each instance, and that instances can no longer be given attributes outside the declared names.

The measurement below runs on CPython 3.14.4 on Linux. Exact numbers vary by build and platform, so run it yourself before you quote it.

python
import sys class PointDict: def __init__(self, x: float, y: float) -> None: self.x = x self.y = y class PointSlots: __slots__ = ("x", "y") def __init__(self, x: float, y: float) -> None: self.x = x self.y = y plain = PointDict(1.0, 2.0) compact = PointSlots(1.0, 2.0) print(sys.getsizeof(plain) + sys.getsizeof(plain.__dict__)) print(sys.getsizeof(compact))

The output is 344 and 48. At one million points the difference is roughly 344 megabytes against 48 megabytes.

The chart compares the two layouts directly.

Bytes per Point instance on CPython 3.14.4
Description

Bar chart of bytes per Point instance on CPython 3.14.4: 344 bytes with a per-instance dictionary and 48 bytes with __slots__, about seven times smaller.

The savings are real, and so are the trade-offs. A slotted class cannot accept a new attribute that is not listed, cannot be weak-referenced unless it declares __weakref__, and cannot use multiple inheritance with another slotted base that has non-empty slots. The data model lists these constraints explicitly.

⚠️Warning

__slots__ is an optimization, not an access control feature. Slotted attributes are still readable and writable from outside. Use it when profiling shows instance dictionaries dominate memory, and measure before and after.

Start with ordinary classes and profile before adding __slots__; for data records, the dataclass decorator generates the slots with slots=True.

7. Equality, Hashing and Value Objects

Two instances are equal only if the class says so. The default object.__eq__ compares identity, so two separately constructed Money(4999) objects differ by default. A value object changes that: two objects with the same fields become interchangeable.

The equality method must respect a contract. Equal objects must have equal hashes, and a hash must not change while the object is used as a key. That is why mutable objects should not implement __hash__. The data model puts the rule plainly: a class that overrides __eq__ and does not define __hash__ gets __hash__ set to None, and its instances become unhashable.

python
class Money: __slots__ = ("cents", "currency") def __init__(self, cents: int, currency: str = "USD") -> None: self.cents = cents self.currency = currency def __repr__(self) -> str: return f"Money({self.cents}, {self.currency!r})" def __eq__(self, other: object) -> bool: if not isinstance(other, Money): return NotImplemented return (self.cents, self.currency) == (other.cents, other.currency) def __hash__(self) -> int: return hash((self.cents, self.currency)) plans = {Money(4999): "Basic", Money(9999): "Pro"} print(plans[Money(4999)]) print(Money(4999) == Money(4999, "USD"))

The output is Basic and True. The dictionary lookup builds a temporary Money and finds the stored key by hash and equality, even though the two objects have different identities.

Returning NotImplemented matters. It tells Python that this class does not know how to compare with that type, so the interpreter can ask the other operand. Returning False instead would silently block a valid comparison implemented by the other class.

The data model documents a default for each of these methods, and the matching exercise above covers the most common ones.

If a class needs all four ordering comparisons, define __eq__ and one of __lt__, __le__, __gt__ or __ge__, then apply functools.total_ordering. The decorator supplies the rest, which removes three near-identical methods.

🚨Danger

Never hash an object whose fields can change. A key that mutates after insertion stays in the wrong hash bucket, so the dictionary can no longer find it, and no error is raised. Use frozen=True on a dataclass or a plain immutable class for keys.

8. Name Mangling Is a Naming Rule

Python has no private attributes. It has a naming convention and one compiler rewrite. An identifier with two leading underscores and at most one trailing underscore, written inside a class body, is rewritten to _ClassName__name. This is called name mangling.

Form
Meaning
Access from outside
name
Public
Direct
_name
Internal by convention
Direct, but discouraged
__name
Mangled to _ClassName__name
Possible if you know the rule
__name__
Reserved by Python
Do not invent new ones

The example below shows the rewrite and its limits.

python
class Session: def __init__(self, token: str) -> None: self.__token = token # stored as _Session__token def masked(self) -> str: return self.__token[-4:].rjust(len(self.__token), "*") session = Session("sk-live-9f2c41") print(session.masked()) print(session._Session__token)

The output is *********9f2c41 and then the raw token. The attribute is reachable. Mangling exists to avoid name clashes between a class and its subclasses, not to protect secrets.

⚠️Warning

Do not store credentials in mangled attributes and call them private. Use a secrets manager, keep the value out of repr, and treat every attribute as readable by anyone who can run code in the process.

9. Dataclasses, Pydantic and the Class You Write

Most classes are records with a little behaviour. Writing __init__, __repr__ and __eq__ by hand for a four-field record is noise, and the dataclass decorator removes it. It generates those methods from annotated fields, plus __hash__, ordering methods and slots.

python
from dataclasses import dataclass @dataclass(frozen=True, slots=True) class Money: cents: int currency: str = "USD" def __str__(self) -> str: return f"{self.cents / 100:.2f} {self.currency}" price = Money(4999) print(price) print(price == Money(4999)) print(hash(price) == hash(Money(4999)))

The output is 49.99 USD, True and True. frozen=True blocks attribute assignment after creation, and the dataclass documentation states that a frozen dataclass with eq=True gets a generated __hash__ by default. slots=True generates a __slots__ attribute and returns a new class, which is the memory optimization from the previous section without the boilerplate.

A dataclass does not validate. It will happily accept Money(-1) or a string in an integer field, because annotations are not enforced at runtime. When data arrives from outside the program, a validating model is the better boundary.

python
from pydantic import BaseModel, Field class InvoiceModel(BaseModel): customer: str amount_cents: int = Field(gt=0) invoice = InvoiceModel(customer="Ada Lovelace", amount_cents=129_900) print(invoice.model_dump())

The output is {'customer': 'Ada Lovelace', 'amount_cents': 129900}. Pydantic builds validation from type hints, with a Rust core in version 2. A negative amount fails before the object exists, which is what a handler needs.

The table compares the three options on the properties that decide the choice.

Approach
Boilerplate removed
Runtime validation
Best fit
Hand-written class
None
Whatever you write
Behaviour, invariants and lifecycle
@dataclass
__init__, __repr__, __eq__
None
Internal records and value objects
Pydantic BaseModel
Parsing, validation, serialization
Yes
Data crossing a trust boundary

The quadrant below turns that table into a placement exercise. The horizontal axis measures how much behaviour the object carries, and the vertical axis measures how many instances the program creates.

100%
Description

A quadrant chart placing a dict and a tuple in the low behaviour, few instance area, dataclass and Pydantic models in the data heavy many instance area, and classes with methods in the behaviour heavy area.

Comparative Verdict: choose a hand-written class when the object owns behaviour, lifecycle or an invariant. Choose a frozen dataclass for values passed inside your own program, and a validated model where untrusted data enters. Validating again in the core loop is the expensive mistake.

10. When Not to Write a Class

A class earns its cost when it holds state that several methods share, or when it enforces an invariant that callers must not break. Without one of those, a function is shorter, easier to test and easier to cache.

Situation
Simpler choice
Why
One transformation of an input
Function
No state to keep
A record with no behaviour
Dataclass or tuple
No hand-written boilerplate
Configuration read once at startup
Frozen dataclass
Immutable and hashable
A family of algorithms with one entry point
Function passed as a value
No class hierarchy needed
State shared by many methods
Class
The state has a home
💡Tip

If a class has an __init__ that stores arguments and a single method that uses them once, it is probably a function wearing a costume. Rewrite it as a function and delete the ceremony.

Real-World Case Studies

pathlib: an object replaced a family of functions

The pathlib module was designed as an object-oriented API for filesystem paths, and PEP 428 explains why the design chose objects over the os.path family of functions. Path objects are immutable, which makes them hashable and prevents a class of programming errors. The immutability is the same idea as the frozen dataclass above, applied to a standard library type.

python
from pathlib import Path report = Path("reports") / "2026-11.csv" print(report.suffix) print(report.parent) print(report.stem)

The output is .csv, reports and 2026-11. The / operator builds a new path instead of mutating the old one.

Django: __str__ is part of the model contract

Django calls str() on model instances in several places, most visibly in the admin site and in templates, and the documentation asks every model to return a readable label from __str__. The model base class supplies the developer-facing repr, and the admin page needs the human form.

python
from django.db import models class Person(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) def __str__(self) -> str: return f"{self.first_name} {self.last_name}"

Without that method, the admin page shows an object address instead of a name, which makes a support tool much harder to use.

📚 Did you know?

  • A slotted class cannot be weak-referenced unless it declares __weakref__, which matters for caches that hold weak keys.
  • A frozendict is hashable, so a mapping can itself be a dictionary key in Python 3.15.

2026 Updates (as of November 2026)

  • Python 3.15.2 is the current stable documentation set, and 3.16 is in development.
  • frozendict is a built-in since 3.15: immutable, hashable and usable as a dictionary key.
  • sentinel() is a built-in since 3.15, with copy support and is based comparison.
  • The 3.15 __slots__ rules allow __dict__ and __weakref__ slots on any class.
  • Dataclasses have accepted slots=True since Python 3.10, so the memory win needs no hand-written slots.

Practice Questions

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A support team reports that adding the tag billing to one ticket also adds it to every other ticket. The Ticket class defines tags: list[str] = [] at class level, and each instance calls self.tags.append(tag). Which change fixes the shared state?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

Which statement about __new__ and __init__ is correct?

Practice Question
How to answer

Multiple-choice question with 2 options. Select one option, then choose Check Answer.

When a method is called on an instance, Python passes that instance as the first argument automatically.

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A class defines both __str__ and __repr__. Which method does print(order) call first?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

Circle.radius_m is a property whose setter raises ValueError for values at or below zero. Circle.__init__ assigns self.radius_m = radius_m. What happens when Circle(0) runs?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

On CPython 3.14.4 a two-float instance with a normal __dict__ measured 344 bytes, while the same class with __slots__ measured 48 bytes. Which explanation matches the data model?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A class defines __eq__ but does not define __hash__. What happens when an instance is used as a dictionary key?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A class stores a token in self.__token. A colleague says the value is now private and safe from other code. Which statement is accurate?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

An internal service passes a four-field record between functions. The record needs value equality, a readable repr, and no mutation after creation, and the team wants no third-party dependency. Which choice removes the most boilerplate?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A script reads a CSV, multiplies one column by 1.2, and writes the result. Which design is the simplest fit?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A configuration loader returns nested dictionaries today. A new feature needs default values, validation and attribute access in twelve modules. Which change is the smallest step that meets those needs?

✅Success

Key Takeaways

  1. A class statement builds a namespace once. Calling the class runs __new__ to allocate and __init__ to fill, and __init__ must return None.
  2. Class attributes are shared, so mutable ones belong in __init__. The shared list bug is a lookup rule, not a typo.
  3. Reading a method through an instance returns a bound method that carries self. Invoice.total(invoice) is the unbound form of the same call.
  4. __repr__ is for developers and should look like a constructor call. __str__ is for users, and print prefers it.
  5. Properties are data descriptors on the class, so they run even for assignments inside __init__. That is how an invariant survives.
  6. __slots__ trades flexibility for memory. On CPython 3.14.4 a two-float instance drops from 344 bytes to 48 bytes, and dataclasses can generate it.
  7. Overriding __eq__ without __hash__ makes instances unhashable. Value objects need both, and only immutable fields may feed the hash.
  8. Name mangling is a naming rule for subclasses, never a security control.
  9. Use a frozen dataclass for internal records, a validated model at the trust boundary, and a hand-written class when behaviour and invariants need a home.

References

Every factual claim links to its source, with the access date printed so the source can be rechecked.

  1. Python Software Foundation · Accessed 2026-11-06

  2. Python Software Foundation · Accessed 2026-11-06

  3. Python Software Foundation · Accessed 2026-11-06

  4. Python Software Foundation · Accessed 2026-11-06

  5. Python Software Foundation · Accessed 2026-11-06

  6. [6]PEP 557, Data ClassesSpecification

    Python Software Foundation · Eric V. Smith · Accessed 2026-11-06

  7. [7]Pydantic overviewDocumentation

    Pydantic · Accessed 2026-11-06

  8. Python Software Foundation · Antoine Pitrou · Accessed 2026-11-06

  9. Django Software Foundation · Accessed 2026-11-06

  10. Python Software Foundation · Accessed 2026-11-06

  11. [11]Python 3.15 documentationDocumentation

    Python Software Foundation · Accessed 2026-11-06

  12. [12]Status of Python versionsDocumentation

    Python Software Foundation · Accessed 2026-11-06

  13. [13]PEP 661, Sentinel ValuesSpecification

    Python Software Foundation · Tal Einat, Jelle Zijlstra · Accessed 2026-11-06

  14. [14]What's New In Python 3.15Documentation

    Python Software Foundation · Published 2026-10-09 · Accessed 2026-11-06

Progress10%