Object-Oriented Programming (OOP) Module 1 – Objects and classes
Static members, class methods and factories
When data belongs to each object and when to the class: static fields, static and class methods, named factories, a shared-list bug and testing.
What you will learn
- Decide whether a piece of data belongs to each object or to the class
- Write static methods, class methods and named factory methods
- Avoid Python's shared mutable class attribute bug
- Predict when static initialisation code runs in Java, Python and JavaScript
- Explain why static state that changes makes code hard to test
Before you start
What you’ll learn · Before you start
What you will learn
- Decide whether a piece of data belongs to each object or to the class
- Write static methods, class methods and named factory methods
- Avoid Python's shared mutable class attribute bug
- Predict when static initialisation code runs in Java, Python and JavaScript
- Explain why static state that changes makes code hard to test
Before you start
On this page
A cinema prints the ticket numbers T-001, T-002 and T-003. Each ticket must remember its own number, yet the next number to hand out cannot live in any one ticket: it belongs to all of them at once. Data like that count, and methods that need no ticket at all, are static member: A field or method that belongs to the class itself, not to its objects: there is one copy for the whole program, and a static method receives no self or this.: they belong to the class instead of to each object.
Below, the counter and a pair of named factories run in 14 languages. Then come the Python trap that shares a list by accident, when static code runs, and why static state that changes is the first thing a test trips over.
Some data belongs to each object, some to the class
The program keeps two kinds of numbers. number is a field of each ticket, set once in the constructor and different
in every ticket. issued belongs to the class: there is exactly one of it, however many tickets exist, and the
constructor adds 1 to it before copying it into the new ticket. Step through the three tickets:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The class alone. Before any ticket exists, the class already holds issued, set to 0. There is one issued for the whole program, however many tickets follow.
- First ticket. The constructor adds 1 to the class's count and copies it into the new ticket's own number field. The ticket keeps 1 for good.
- Second ticket. The same constructor runs for the next ticket. The count becomes 2 and the second ticket keeps 2; the first ticket still holds 1.
- Third ticket. The evening ticket makes the count 3 and keeps 3 as its number. Three numbers, one in each ticket, and still one count, in the class.
- A static method. Ticket.label(2) turns a number into "T-002". It reads no ticket and no count, so it is static, called on the class with no object at all.
The same program in 14 languages. The way each language spells “belongs to the class” varies more than anything else in this module, and the notes say what each spelling means:
Python 3.14 · main.py
class Ticket:
issued = 0 # one count, shared by every ticket
def __init__(self, show: str) -> None:
Ticket.issued += 1
self.number = Ticket.issued # each ticket's own number
self.show = show
@staticmethod
def label(number: int) -> str:
return f"T-{number:03d}"
tickets = [Ticket("matinee"), Ticket("matinee"), Ticket("evening")]
for ticket in tickets:
print(f"{Ticket.label(ticket.number)} for the {ticket.show} show")
print(f"issued so far: {Ticket.issued}") In Python: issued is a class attribute, read and changed as Ticket.issued. self.issued += 1 would instead give one ticket an attribute of its own and leave the class's count at 0.
Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py
JavaScript Node 24 · main.mjs
class Ticket {
static #issued = 0; // one count, shared by every ticket
constructor(show) {
Ticket.#issued += 1;
this.number = Ticket.#issued; // each ticket's own number
this.show = show;
}
static get issued() {
return Ticket.#issued;
}
static label(number) {
return `T-${String(number).padStart(3, "0")}`;
}
}
const tickets = [new Ticket("matinee"), new Ticket("matinee"), new Ticket("evening")];
for (const ticket of tickets) {
console.log(`${Ticket.label(ticket.number)} for the ${ticket.show} show`);
}
console.log(`issued so far: ${Ticket.issued}`); In JavaScript: static #issued belongs to the class and is private; the static getter issued and the static method label are called on Ticket itself, never on a ticket.
Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs
Java 25 LTS · Main.java
import java.util.List;
import java.util.Locale;
public class Main {
public static void main(String[] args) {
Locale.setDefault(Locale.ROOT);
List<Ticket> tickets =
List.of(new Ticket("matinee"), new Ticket("matinee"), new Ticket("evening"));
for (Ticket ticket : tickets) {
System.out.println(Ticket.label(ticket.number()) + " for the " + ticket.show() + " show");
}
System.out.println("issued so far: " + Ticket.issuedSoFar());
}
}
final class Ticket {
private static int issued = 0; // one count, shared by every ticket
private final int number;
private final String show;
Ticket(String show) {
issued += 1;
this.number = issued; // each ticket's own number
this.show = show;
}
int number() {
return number;
}
String show() {
return show;
}
static int issuedSoFar() {
return issued;
}
static String label(int number) {
return String.format("T-%03d", number);
}
} In Java: static int issued has one copy for the class. A static method such as label has no this, so it can use only its parameters and other static members.
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java
C++23 · main.cpp
#include <format>
#include <print>
#include <string>
#include <utility>
#include <vector>
class Ticket {
public:
explicit Ticket(std::string show) : show_(std::move(show)) {
issued_ += 1;
number_ = issued_; // each ticket's own number
}
int number() const { return number_; }
const std::string &show() const { return show_; }
static int issued() { return issued_; }
static std::string label(int number) {
return std::format("T-{:03}", number);
}
private:
inline static int issued_ = 0; // one count, shared by every ticket
int number_ = 0;
std::string show_;
};
int main() {
std::vector<Ticket> tickets;
tickets.emplace_back("matinee");
tickets.emplace_back("matinee");
tickets.emplace_back("evening");
for (const Ticket &ticket : tickets) {
std::println("{} for the {} show", Ticket::label(ticket.number()),
ticket.show());
}
std::println("issued so far: {}", Ticket::issued());
} In C++: inline static, new in C++17, defines the shared count inside the class. A static member function has no this pointer, so label could not read number_.
Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main
C# 14 · .NET 10 · app.cs
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Ticket[] tickets = [new("matinee"), new("matinee"), new("evening")];
foreach (var ticket in tickets)
{
Console.WriteLine($"{Ticket.Label(ticket.Number)} for the {ticket.Show} show");
}
Console.WriteLine($"issued so far: {Ticket.Issued}");
sealed class Ticket
{
public static int Issued { get; private set; } // one count, shared by every ticket
public int Number { get; }
public string Show { get; }
public Ticket(string show)
{
Issued += 1;
Number = Issued; // each ticket's own number
Show = show;
}
public static string Label(int number) => $"T-{number:D3}";
} In C#: A static auto-property with private set belongs to the type itself. Code reads Ticket.Issued, and only the class can change it.
Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs
C23 · main.c
#include <stddef.h>
#include <stdio.h>
static int tickets_issued = 0; // one count, shared by every ticket
struct ticket {
int number;
const char *show;
};
static struct ticket ticket_new(const char *show) {
tickets_issued += 1;
return (struct ticket){.number = tickets_issued, // each ticket's own number
.show = show};
}
static void ticket_label(int number, char *out, size_t size) {
snprintf(out, size, "T-%03d", number);
}
int main(void) {
const struct ticket first = ticket_new("matinee");
const struct ticket second = ticket_new("matinee");
const struct ticket third = ticket_new("evening");
const struct ticket tickets[] = {first, second, third};
char label[16];
for (size_t i = 0; i < sizeof tickets / sizeof tickets[0]; i++) {
ticket_label(tickets[i].number, label, sizeof label);
printf("%s for the %s show\n", label, tickets[i].show);
}
printf("issued so far: %d\n", tickets_issued);
return 0;
} In C: C has no classes, so the shared count is a variable at file scope, where static means private to this file. C leaves the order of the calls in one initializer list unspecified, so each ticket is made in a statement of its own.
Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main
PHP 8.5 · main.php
<?php
declare(strict_types=1);
final class Ticket
{
private static int $issued = 0; // one count, shared by every ticket
public readonly int $number;
public function __construct(public readonly string $show)
{
self::$issued += 1;
$this->number = self::$issued; // each ticket's own number
}
public static function issuedSoFar(): int
{
return self::$issued;
}
public static function label(int $number): string
{
return sprintf('T-%03d', $number);
}
}
$tickets = [new Ticket('matinee'), new Ticket('matinee'), new Ticket('evening')];
foreach ($tickets as $ticket) {
echo Ticket::label($ticket->number) . " for the {$ticket->show} show\n";
}
echo 'issued so far: ' . Ticket::issuedSoFar() . "\n"; In PHP: self::$issued names the class's own static property, and readonly properties, from PHP 8.1, may be written only once, in the constructor.
Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php
Rust 1.99 · main.rs
use std::sync::atomic::{AtomicU32, Ordering};
static ISSUED: AtomicU32 = AtomicU32::new(0); // one count, shared by every ticket
struct Ticket {
number: u32,
show: String,
}
impl Ticket {
fn new(show: &str) -> Self {
let number = ISSUED.fetch_add(1, Ordering::Relaxed) + 1; // each ticket's own number
Ticket {
number,
show: show.to_string(),
}
}
fn issued() -> u32 {
ISSUED.load(Ordering::Relaxed)
}
fn label(number: u32) -> String {
format!("T-{number:03}")
}
}
fn main() {
let tickets = [
Ticket::new("matinee"),
Ticket::new("matinee"),
Ticket::new("evening"),
];
for ticket in &tickets {
println!(
"{} for the {} show",
Ticket::label(ticket.number),
ticket.show
);
}
println!("issued so far: {}", Ticket::issued());
} In Rust: A Rust type has no static fields, and a static mut needs unsafe code, so the count is an atomic static. Ticket::label is an associated function, Rust's static method.
Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main
TypeScript 6.0 · main.ts
class Ticket {
static #issued = 0; // one count, shared by every ticket
readonly number: number;
readonly show: string;
constructor(show: string) {
Ticket.#issued += 1;
this.number = Ticket.#issued; // each ticket's own number
this.show = show;
}
static get issued(): number {
return Ticket.#issued;
}
static label(number: number): string {
return `T-${String(number).padStart(3, "0")}`;
}
}
const tickets = [new Ticket("matinee"), new Ticket("matinee"), new Ticket("evening")];
for (const ticket of tickets) {
console.log(`${Ticket.label(ticket.number)} for the ${ticket.show} show`);
}
console.log(`issued so far: ${Ticket.issued}`); Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts
Go 1.27 · main.go
package main
import "fmt"
var issued int // one count, shared by every ticket
type Ticket struct {
number int
show string
}
func NewTicket(show string) *Ticket {
issued++
return &Ticket{number: issued, show: show} // each ticket's own number
}
func Label(number int) string {
return fmt.Sprintf("T-%03d", number)
}
func main() {
tickets := []*Ticket{NewTicket("matinee"), NewTicket("matinee"), NewTicket("evening")}
for _, ticket := range tickets {
fmt.Printf("%s for the %s show\n", Label(ticket.number), ticket.show)
}
fmt.Printf("issued so far: %d\n", issued)
} In Go: Go has no static members. A package-level variable holds the shared count, and Label is a plain function of the package.
Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go
Swift 6.4 · main.swift
final class Ticket {
private(set) static var issued = 0 // one count, shared by every ticket
let number: Int
let show: String
init(show: String) {
Ticket.issued += 1
number = Ticket.issued // each ticket's own number
self.show = show
}
static func label(_ number: Int) -> String {
let digits = String(number)
return "T-" + String(repeating: "0", count: max(0, 3 - digits.count)) + digits
}
}
let tickets = [Ticket(show: "matinee"), Ticket(show: "matinee"), Ticket(show: "evening")]
for ticket in tickets {
print("\(Ticket.label(ticket.number)) for the \(ticket.show) show")
}
print("issued so far: \(Ticket.issued)") In Swift: static var issued belongs to the class, and private(set) lets only Ticket change it. Static methods are called on the type, as in Ticket.label(2).
Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main
Ruby 4.0 · main.rb
# frozen_string_literal: true
# A ticket for one show, numbered from a count kept by the class.
class Ticket
@issued = 0 # one count, shared by every ticket
class << self
attr_reader :issued
def label(number)
"T-#{number.to_s.rjust(3, '0')}"
end
def next_number
@issued += 1
end
end
attr_reader :number, :show
def initialize(show)
@number = Ticket.next_number # each ticket's own number
@show = show
end
end
tickets = [Ticket.new('matinee'), Ticket.new('matinee'), Ticket.new('evening')]
tickets.each do |ticket|
puts "#{Ticket.label(ticket.number)} for the #{ticket.show} show"
end
puts "issued so far: #{Ticket.issued}" In Ruby: @issued in the class body is an instance variable of the class object itself, which class << self exposes. A @@ class variable would also be shared with subclasses.
Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb
Kotlin 2.4 · Main.kt
class Ticket(
val show: String,
) {
val number: Int
init {
issued += 1
number = issued // each ticket's own number
}
companion object {
var issued = 0 // one count, shared by every ticket
private set
fun label(number: Int): String = "T-${number.toString().padStart(3, '0')}"
}
}
fun main() {
val tickets = listOf(Ticket("matinee"), Ticket("matinee"), Ticket("evening"))
for (ticket in tickets) {
println("${Ticket.label(ticket.number)} for the ${ticket.show} show")
}
println("issued so far: ${Ticket.issued}")
} In Kotlin: Kotlin has no static keyword. Members of the companion object belong to the class, and the init block runs once for each new ticket.
Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
Dart 3.13 · main.dart
class Ticket {
Ticket(this.show) : number = ++_issued; // each ticket's own number
static int _issued = 0; // one count, shared by every ticket
final int number;
final String show;
static int get issued => _issued;
static String label(int number) => 'T-${number.toString().padLeft(3, '0')}';
}
void main() {
final tickets = [Ticket('matinee'), Ticket('matinee'), Ticket('evening')];
for (final ticket in tickets) {
print('${Ticket.label(ticket.number)} for the ${ticket.show} show');
}
print('issued so far: ${Ticket.issued}');
} In Dart: Static members belong to the class. The initializer list, number = ++_issued, sets the final field before the constructor's body runs.
Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart
What every language printed
T-001 for the matinee show T-002 for the matinee show T-003 for the evening show issued so far: 3
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 65 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 151 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 2 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 8 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 20 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 716 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 180 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 9 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
All 14 printed the same four lines: three tickets numbered 1 to 3, and one count of 3 read from the class. Python
calls issued a class attribute: In Python, a name assigned in the class body. It belongs to the class and is found through every object that has no attribute of its own by that name.; Java, C#, C++, JavaScript, TypeScript, PHP,
Swift and Dart mark it static; Kotlin puts it in a companion object. Go and C, which have no classes, use a variable
at the level of the package or the file, and Rust uses a static item outside the type, an atomic counter, because a
static mut would need unsafe code.
The test for where a piece of data belongs is to ask whether it can differ between two objects. A ticket’s number, seat and show can, so they are instance fields. The count of tickets issued, a conversion factor or the longest allowed name cannot, so they belong to the class.
Key idea
Data that may differ between two objects is an instance field; data that is one value for the whole kind of object, such as the count of tickets issued, is a static member and lives in the class, once.
Checkpoint
Check: Static members and instance fields
Static methods need no object, and named factories are static methods
Ticket.label(2) turns a number into “T-002” without looking at any ticket. A method like that is static: it has no
receiver, so it cannot read an instance field. Java’s tutorial puts the rule exactly: class methods “cannot use the
this keyword as there is no instance for this to refer to”. Python spells the same thing @staticmethod, a method
that, in the words of its documentation, “does not receive an implicit first argument”.
A common use of static methods is the named factory: A static method that creates and returns an object, with a name that says what its arguments mean, such as Distance.from_kilometres(5) or Duration.ofMinutes(90).. A constructor has no name of
its own, so Distance(5) cannot say whether it means 5 metres or 5 kilometres. Two static methods can:
Distance.from_kilometres(5) and Distance.from_metres(400). Each converts to metres, and the one constructor checks
the value for both. Several languages also make the constructor private, so that the factories are the only way in:
Python 3.14 · main.py
class Distance:
def __init__(self, metres: int) -> None:
if metres < 0:
raise ValueError(f"a distance cannot be negative: {metres} m")
self.metres = metres
# Named factories: each name says which unit its number is in.
@classmethod
def from_metres(cls, metres: int) -> "Distance":
return cls(metres)
@classmethod
def from_kilometres(cls, kilometres: int) -> "Distance":
return cls(kilometres * 1000)
run = Distance.from_kilometres(5)
lap = Distance.from_metres(400)
print(f"run: {run.metres} m, lap: {lap.metres} m")
print(
f"laps in the run: {run.metres // lap.metres}, then {run.metres % lap.metres} m more"
)
try:
Distance.from_kilometres(-2)
except ValueError as error:
print(f"refused: {error}") In Python: @classmethod passes the class as cls, so cls(kilometres * 1000) builds whatever class the factory was called on. Python cannot make __init__ private, so Distance(5000) still works.
Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py
JavaScript Node 24 · main.mjs
class Distance {
constructor(metres) {
if (metres < 0) {
throw new RangeError(`a distance cannot be negative: ${metres} m`);
}
this.metres = metres;
}
// Named factories: each name says which unit its number is in.
static fromMetres(metres) {
return new Distance(metres);
}
static fromKilometres(kilometres) {
return new Distance(kilometres * 1000);
}
}
const run = Distance.fromKilometres(5);
const lap = Distance.fromMetres(400);
console.log(`run: ${run.metres} m, lap: ${lap.metres} m`);
const laps = Math.trunc(run.metres / lap.metres);
console.log(`laps in the run: ${laps}, then ${run.metres % lap.metres} m more`);
try {
Distance.fromKilometres(-2);
} catch (error) {
console.log(`refused: ${error.message}`);
} In JavaScript: A JavaScript constructor cannot be private, so new Distance(5000) still works beside the factories. Math.trunc stands in for integer division.
Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs
Java 25 LTS · Main.java
public class Main {
public static void main(String[] args) {
Distance run = Distance.fromKilometres(5);
Distance lap = Distance.fromMetres(400);
System.out.println("run: " + run.metres() + " m, lap: " + lap.metres() + " m");
int laps = run.metres() / lap.metres();
System.out.println(
"laps in the run: " + laps + ", then " + run.metres() % lap.metres() + " m more");
try {
Distance.fromKilometres(-2);
} catch (IllegalArgumentException error) {
System.out.println("refused: " + error.getMessage());
}
}
}
record Distance(int metres) {
Distance {
if (metres < 0) {
throw new IllegalArgumentException("a distance cannot be negative: " + metres + " m");
}
}
// Named factories: each name says which unit its number is in.
static Distance fromMetres(int metres) {
return new Distance(metres);
}
static Distance fromKilometres(int kilometres) {
return new Distance(kilometres * 1000);
}
} In Java: Distance is a record whose compact constructor checks the value. A record with package access may not make that constructor private, so new Distance(5000) still compiles. The java.time classes name such factories of…, as in Duration.ofMinutes(90).
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java
C++23 · main.cpp
#include <format>
#include <print>
#include <stdexcept>
class Distance {
public:
// Named factories: each name says which unit its number is in.
static Distance from_metres(int metres) { return Distance(metres); }
static Distance from_kilometres(int kilometres) {
return Distance(kilometres * 1000);
}
int metres() const { return metres_; }
private:
explicit Distance(int metres) : metres_(metres) {
if (metres < 0) {
throw std::invalid_argument(
std::format("a distance cannot be negative: {} m", metres));
}
}
int metres_;
};
int main() {
const Distance run = Distance::from_kilometres(5);
const Distance lap = Distance::from_metres(400);
std::println("run: {} m, lap: {} m", run.metres(), lap.metres());
std::println("laps in the run: {}, then {} m more",
run.metres() / lap.metres(), run.metres() % lap.metres());
try {
Distance::from_kilometres(-2);
} catch (const std::invalid_argument &error) {
std::println("refused: {}", error.what());
}
} In C++: The constructor is private, so the two factories are the only way to make a Distance, and explicit stops an int from turning into one by accident.
Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main
C# 14 · .NET 10 · app.cs
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
var run = Distance.FromKilometres(5);
var lap = Distance.FromMetres(400);
Console.WriteLine($"run: {run.Metres} m, lap: {lap.Metres} m");
int laps = run.Metres / lap.Metres;
Console.WriteLine($"laps in the run: {laps}, then {run.Metres % lap.Metres} m more");
try
{
Distance.FromKilometres(-2);
}
catch (ArgumentException error)
{
Console.WriteLine($"refused: {error.Message}");
}
sealed class Distance
{
public int Metres { get; }
private Distance(int metres)
{
if (metres < 0)
{
throw new ArgumentException($"a distance cannot be negative: {metres} m");
}
Metres = metres;
}
// Named factories: each name says which unit its number is in.
public static Distance FromMetres(int metres) => new(metres);
public static Distance FromKilometres(int kilometres) => new(kilometres * 1000);
} In C#: The private constructor leaves FromMetres and FromKilometres as the only way in. The TimeSpan.FromMinutes(90) of .NET is the same pattern.
Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs
C23 · main.c
#include <stddef.h>
#include <stdio.h>
struct distance {
int metres;
};
// Named factories: each name says which unit its number is in.
static bool distance_from_metres(int metres, struct distance *out, char *why,
size_t size) {
if (metres < 0) {
snprintf(why, size, "a distance cannot be negative: %d m", metres);
return false;
}
out->metres = metres;
return true;
}
static bool distance_from_kilometres(int kilometres, struct distance *out,
char *why, size_t size) {
return distance_from_metres(kilometres * 1000, out, why, size);
}
int main(void) {
char why[64];
struct distance run;
struct distance lap;
distance_from_kilometres(5, &run, why, sizeof why);
distance_from_metres(400, &lap, why, sizeof why);
printf("run: %d m, lap: %d m\n", run.metres, lap.metres);
printf("laps in the run: %d, then %d m more\n", run.metres / lap.metres,
run.metres % lap.metres);
struct distance refused;
if (!distance_from_kilometres(-2, &refused, why, sizeof why)) {
printf("refused: %s\n", why);
}
return 0;
} In C: C has no static methods. The factories are functions that fill in a struct and report a refusal through their return value and a message buffer.
Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main
PHP 8.5 · main.php
<?php
declare(strict_types=1);
final class Distance
{
private function __construct(public readonly int $metres)
{
if ($metres < 0) {
throw new InvalidArgumentException("a distance cannot be negative: {$metres} m");
}
}
// Named factories: each name says which unit its number is in.
public static function fromMetres(int $metres): self
{
return new self($metres);
}
public static function fromKilometres(int $kilometres): self
{
return new self($kilometres * 1000);
}
}
$run = Distance::fromKilometres(5);
$lap = Distance::fromMetres(400);
echo "run: {$run->metres} m, lap: {$lap->metres} m\n";
$laps = intdiv($run->metres, $lap->metres);
echo "laps in the run: {$laps}, then " . $run->metres % $lap->metres . " m more\n";
try {
Distance::fromKilometres(-2);
} catch (InvalidArgumentException $error) {
echo "refused: {$error->getMessage()}\n";
} In PHP: The constructor is private, and self, in the return types and in new self(…), names the class itself.
Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php
Rust 1.99 · main.rs
struct Distance {
metres: i32,
}
impl Distance {
fn new(metres: i32) -> Result<Self, String> {
if metres < 0 {
return Err(format!("a distance cannot be negative: {metres} m"));
}
Ok(Distance { metres })
}
// Named factories: each name says which unit its number is in.
fn from_metres(metres: i32) -> Result<Self, String> {
Distance::new(metres)
}
fn from_kilometres(kilometres: i32) -> Result<Self, String> {
Distance::new(kilometres * 1000)
}
}
fn main() -> Result<(), String> {
let run = Distance::from_kilometres(5)?;
let lap = Distance::from_metres(400)?;
println!("run: {} m, lap: {} m", run.metres, lap.metres);
println!(
"laps in the run: {}, then {} m more",
run.metres / lap.metres,
run.metres % lap.metres
);
if let Err(why) = Distance::from_kilometres(-2) {
println!("refused: {why}");
}
Ok(())
} In Rust: Rust has no constructors. new and the from_ functions are associated functions, Rust's static methods, and each returns a Result.
Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main
TypeScript 6.0 · main.ts
class Distance {
readonly metres: number;
constructor(metres: number) {
if (metres < 0) {
throw new RangeError(`a distance cannot be negative: ${metres} m`);
}
this.metres = metres;
}
// Named factories: each name says which unit its number is in.
static fromMetres(metres: number): Distance {
return new Distance(metres);
}
static fromKilometres(kilometres: number): Distance {
return new Distance(kilometres * 1000);
}
}
const run = Distance.fromKilometres(5);
const lap = Distance.fromMetres(400);
console.log(`run: ${run.metres} m, lap: ${lap.metres} m`);
const laps = Math.trunc(run.metres / lap.metres);
console.log(`laps in the run: ${laps}, then ${run.metres % lap.metres} m more`);
try {
Distance.fromKilometres(-2);
} catch (error) {
console.log(`refused: ${error instanceof Error ? error.message : String(error)}`);
} In TypeScript: TypeScript could mark the constructor private, a check the compiler alone makes; this version keeps it public, as the JavaScript one must.
Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts
Go 1.27 · main.go
package main
import "fmt"
type Distance struct {
metres int
}
func newDistance(metres int) (Distance, error) {
if metres < 0 {
return Distance{}, fmt.Errorf("a distance cannot be negative: %d m", metres)
}
return Distance{metres: metres}, nil
}
// Named factories: each name says which unit its number is in.
func FromMetres(metres int) (Distance, error) {
return newDistance(metres)
}
func FromKilometres(kilometres int) (Distance, error) {
return newDistance(kilometres * 1000)
}
func main() {
run, err := FromKilometres(5)
if err != nil {
fmt.Println(err)
return
}
lap, err := FromMetres(400)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("run: %d m, lap: %d m\n", run.metres, lap.metres)
laps := run.metres / lap.metres
fmt.Printf("laps in the run: %d, then %d m more\n", laps, run.metres%lap.metres)
if _, err := FromKilometres(-2); err != nil {
fmt.Printf("refused: %v\n", err)
}
} In Go: Go has no static methods. FromMetres and FromKilometres are package functions, and lowercase newDistance cannot be called from another package.
Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go
Swift 6.4 · main.swift
struct Refused: Error, CustomStringConvertible {
let description: String
}
struct Distance {
let metres: Int
private init(metres: Int) throws(Refused) {
guard metres >= 0 else {
throw Refused(description: "a distance cannot be negative: \(metres) m")
}
self.metres = metres
}
// Named factories: each name says which unit its number is in.
static func fromMetres(_ metres: Int) throws(Refused) -> Distance {
try Distance(metres: metres)
}
static func fromKilometres(_ kilometres: Int) throws(Refused) -> Distance {
try Distance(metres: kilometres * 1000)
}
}
let run = try Distance.fromKilometres(5)
let lap = try Distance.fromMetres(400)
print("run: \(run.metres) m, lap: \(lap.metres) m")
print("laps in the run: \(run.metres / lap.metres), then \(run.metres % lap.metres) m more")
do {
_ = try Distance.fromKilometres(-2)
} catch {
print("refused: \(error)")
} In Swift: The initializer is private, so from outside the type the static functions are the only way in, and typed throws report the refused value.
Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main
Ruby 4.0 · main.rb
# frozen_string_literal: true
# A distance kept in whole metres.
class Distance
attr_reader :metres
def initialize(metres)
raise ArgumentError, "a distance cannot be negative: #{metres} m" if metres.negative?
@metres = metres
end
# Named factories: each name says which unit its number is in.
def self.from_metres(metres)
new(metres)
end
def self.from_kilometres(kilometres)
new(kilometres * 1000)
end
end
run = Distance.from_kilometres(5)
lap = Distance.from_metres(400)
puts "run: #{run.metres} m, lap: #{lap.metres} m"
puts "laps in the run: #{run.metres / lap.metres}, then #{run.metres % lap.metres} m more"
begin
Distance.from_kilometres(-2)
rescue ArgumentError => e
puts "refused: #{e.message}"
end In Ruby: def self.from_metres defines a method on the class itself. new stays public unless private_class_method :new hides it.
Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb
Kotlin 2.4 · Main.kt
class Distance private constructor(
val metres: Int,
) {
init {
require(metres >= 0) { "a distance cannot be negative: $metres m" }
}
// Named factories: each name says which unit its number is in.
companion object {
fun fromMetres(metres: Int) = Distance(metres)
fun fromKilometres(kilometres: Int) = Distance(kilometres * 1000)
}
}
fun main() {
val run = Distance.fromKilometres(5)
val lap = Distance.fromMetres(400)
println("run: ${run.metres} m, lap: ${lap.metres} m")
println("laps in the run: ${run.metres / lap.metres}, then ${run.metres % lap.metres} m more")
try {
Distance.fromKilometres(-2)
} catch (error: IllegalArgumentException) {
println("refused: ${error.message}")
}
} In Kotlin: The private constructor and the companion object's factories play the parts of a private constructor and static methods; init checks every new Distance.
Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
Dart 3.13 · main.dart
class Distance {
// Named factories: each name says which unit its number is in.
Distance.fromMetres(this.metres) {
if (metres < 0) {
throw ArgumentError('a distance cannot be negative: $metres m');
}
}
Distance.fromKilometres(int kilometres) : this.fromMetres(kilometres * 1000);
final int metres;
}
void main() {
final run = Distance.fromKilometres(5);
final lap = Distance.fromMetres(400);
print('run: ${run.metres} m, lap: ${lap.metres} m');
print(
'laps in the run: ${run.metres ~/ lap.metres}, then ${run.metres % lap.metres} m more',
);
try {
Distance.fromKilometres(-2);
} on ArgumentError catch (error) {
print('refused: ${error.message}');
}
} In Dart: Dart writes named factories as named constructors, Distance.fromMetres(…). With no unnamed constructor, Distance(5000) does not compile.
Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart
What every language printed
run: 5000 m, lap: 400 m laps in the run: 12, then 200 m more refused: a distance cannot be negative: -2000 m
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 9 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 153 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 2 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 8 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 24 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 724 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 179 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 9 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
Every language printed the same three lines, including the refusal of -2 kilometres, which became -2000 metres before
the one check saw it. The pattern is everywhere in standard libraries: .NET’s TimeSpan.FromMinutes(90) and Java’s
Duration.ofMinutes(90) are named factories, and Dart writes them as named constructors.
Python has a second kind, the class method: In Python, a method marked @classmethod that receives the class itself as its first argument, cls, so a factory called on a subclass builds the subclass.. Its documentation says it “receives the
class as an implicit first argument”, and when it is called on a subclass, “the derived class object is passed”. The
next program calls both kinds of factory on a subclass, RaceDistance:
class Distance:
def __init__(self, metres: int) -> None:
self.metres = metres
@classmethod
def by_classmethod(cls, kilometres: int) -> "Distance":
return cls(kilometres * 1000) # cls is the class the call was made on
@staticmethod
def by_staticmethod(kilometres: int) -> "Distance":
return Distance(kilometres * 1000) # always a Distance
class RaceDistance(Distance):
"""A distance a race is run over."""
for factory in (RaceDistance.by_classmethod, RaceDistance.by_staticmethod):
made = factory(5)
print(f"{factory.__name__}: a {type(made).__name__} of {made.metres} m") Output
by_classmethod: a RaceDistance of 5000 m by_staticmethod: a Distance of 5000 m
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 factory_kinds.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The class method built a RaceDistance, because cls was the class it was called on; the static method built a plain
Distance, because it names Distance itself. Ruby’s class methods, a JavaScript static method that calls
new this(…) and PHP’s new static behave like Python’s class method. In Java, C#, C++ and Kotlin a static method
receives no class at all, so a factory there builds exactly the class it names.
Checkpoint
Check: Static methods and named factories
A list in a Python class body is shared by every object
In Java, C#, TypeScript, Kotlin and Dart, a field written in the class body with a starting value, such as
List<String> members = new ArrayList<>();, gets a new value for every object. Python reads the same-looking line
differently. A class body is code that runs once, when the class statement runs, and every name it assigns becomes a
class attribute. A list made there is one list, which every object of the class finds when it looks up the name. Step
through the bug:
Program · shared/shared_list_bug.py
class Team:
members = [] # in the class body
red = Team()
blue = Team()
red.members.append("Asha")
reds = red.members
blues = blue.members
print(blues)
print(reds is blues) Variables
- changed in this step
Printed so far
Nothing printed yet['Asha'] True
Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.
The drawing shows the variables when the program ends, and the program's 9 steps are listed below.
Change the program and run it here. Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Every step as text
- Start. No line has run yet, so there are no variables. Python starts with line 1.
- Line 1.
class Team:makes the classTeam. Its body runs only when it is used. Key point The class statement runs its body once, here.members = []makes one list and stores it in the classTeam, not in any team. - Line 5.
red = Team()setsredto<Team object>. - Line 6.
blue = Team()setsblueto<Team object>. - Line 7.
red.members.append("Asha")runs. The bugredhas nomembersof its own, sored.membersfinds the list in the class, andappendchanges that one list. - Line 8.
reds = red.memberssetsredsto['Asha']. - Line 9.
blues = blue.membersmakesbluesa second name for the listredsnames: one list, two names, not a copy. The bugblue.membersfinds the same list in the class:bluenever had a list of its own. - Line 10.
print(blues)prints['Asha']. - Line 11.
print(reds is blues)printsTrue. That was the last line, so the program ends. One list for everyTeam, so Asha appears in blue's members too. The fix creates the list in__init__, once for each new team.
blues is the same list as reds, and reds is blues printed True: Asha joined both teams with one append. The
Python tutorial warns about exactly this with a list of tricks in its own example: a mutable object in the class body
is shared by all instances. The fix creates the list in __init__, which runs once for each new object:
class Team:
def __init__(self) -> None:
self.members: list[str] = [] # a new list for each team
red = Team()
blue = Team()
red.members.append("Asha")
print(red.members, blue.members)
print(red.members is blue.members) Output
['Asha'] [] False
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shared_list_fix.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Now red.members is blue.members is False, and blue’s list stays empty while red’s holds Asha.
Changing a class attribute through self
self.issued += 1 in the ticket’s constructor does not change the class’s count. It reads Ticket.issued, adds 1
and stores the result as a new attribute of that one ticket, so the class still says 0 and every ticket gets 1.
Write Ticket.issued += 1, as the program does.
A shared mutable class attribute is not always a bug: when every object really should see one collection, such as a registry of all teams, a class-level list is the honest way to say so. The bug is reaching for it by accident, when each object should have one of its own.
Checkpoint
Check: Python's shared class attributes
Static code runs once for the class, not once for each object
Static fields need starting values, and sometimes code to compute them. That code runs once, but when depends on the language. In Java it runs when the class is first used: the Java Language Specification lists the triggers, such as creating an instance, calling a static method or assigning a static field. The next program shows the order:
public class InitOrder {
public static void main(String[] args) {
System.out.println("main starts");
System.out.println("first ticket: " + new Ticket().number);
System.out.println("second ticket: " + new Ticket().number);
}
}
class Ticket {
static int issued;
final int number;
static {
System.out.println("Ticket's static initializer runs, once");
}
Ticket() {
issued += 1;
number = issued;
System.out.println("constructor runs for ticket " + number);
}
} Output
main starts Ticket's static initializer runs, once constructor runs for ticket 1 first ticket: 1 constructor runs for ticket 2 second ticket: 2
Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java InitOrder.java
“main starts” came first, so nothing about Ticket ran when the program started. The static initializer ran once,
just before the first ticket was created, and the constructor ran for each ticket. Other languages pick another
moment:
| Language | Static initialisation runs |
|---|---|
| Java | Once, just before the class is first used (an instance created, a static method called, a static field used) |
| Python | Once, when the class statement itself runs, usually when the module is imported |
| JavaScript, TypeScript | Once, when the class definition is evaluated: static fields and static { … } blocks in order |
| C++ | An inline static int with a constant value is ready before main starts |
| C# | Once, at the latest before the first instance is made or a static member is used |
| Kotlin | A companion object once, when its class is loaded, as a Java static initializer; init blocks for each object |
The useful consequence: work in static initialisation is paid once, so it suits values that are the same for the whole program, such as a table of conversion factors. It does not suit anything that should be fresh for every object or every test.
Checkpoint
Check: When static code runs
Static state that changes makes tests depend on their order
A test suite runs many tests in one program, so they all see the same static fields. A static field that changes,
such as the ticket count, carries what one test did into the next. The program below runs two small tests of the
ticket class, then the same two tests of a TicketOffice object that owns its count:
class Ticket:
issued = 0 # static state, shared by every test
def __init__(self) -> None:
Ticket.issued += 1
self.number = Ticket.issued
class TicketOffice:
def __init__(self) -> None:
self.issued = 0 # instance state: each office counts for itself
def issue(self) -> int:
self.issued += 1
return self.issued
# Two tests of the static version: the second passes only when it runs first.
print("static, test 1:", Ticket().number == 1)
print("static, test 2:", [Ticket().number, Ticket().number] == [1, 2])
# The same two tests, each with an office of its own.
print("office, test 1:", TicketOffice().issue() == 1)
office = TicketOffice()
print("office, test 2:", [office.issue(), office.issue()] == [1, 2]) Output
static, test 1: True static, test 2: False office, test 1: True office, test 2: True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 order_dependence.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The second static test failed, though the class did exactly what it was written to do: the first test had already
issued ticket 1, so the second test’s tickets were 2 and 3. Run the tests in the other order and the other one fails.
With a TicketOffice per test, each test starts from its own count of 0 and both pass in any order.
That is why careful code keeps changing state out of static fields. Give the state to an object, here an office, and
hand that object to the code that needs it, so a test can pass in a fresh one. A module on dependencies and
testability builds on this. What stays static is what never changes, such as constants, and functions that read
only their arguments, such as label.
Key idea
Static fields that change are shared by every test in the program, so tests start to depend on their order; put such state in an object that each test can make fresh, and keep static members for constants and pure functions.
Checkpoint
Check: Static state and tests
Practise: order numbers counted per prefix
The exercise asks for a class whose counts are deliberately shared: one count for each prefix, kept by the class, so
that the kitchen’s orders run KIT-1, KIT-2 while the library’s run LIB-1. Its sample tests run in your browser, and
each one starts by calling the class’s reset(), because, as the last section showed, a count kept by the class
carries over from one test to the next:
Exercise · Easy · Python, JavaScript
Order numbers counted per prefix by the class
A canteen and a library share one ordering system, and each counts its own orders: the kitchen's are KIT-1, KIT-2 and so on, the library's LIB-1, LIB-2. Write the class Order in orders.py (JavaScript: orders.mjs):
Order(prefix)makes an order whoseidis the prefix, a hyphen and the next number for that prefix:Order("KIT"),Order("KIT")andOrder("LIB")getKIT-1,KIT-2andLIB-1. An empty prefix raisesValueError(JavaScript:RangeError).- Each order keeps its own
idfor good; making later orders never changes it. Order.issued(prefix)returns how many orders that prefix has issued, 0 for a prefix never used. It is called on the class, with no order.Order.reset()clears every count, so that a test can start from nothing.
The counts belong to the class, not to any order, and every prefix has a count of its own. The sample tests call Order.reset() first, make orders under several prefixes and check the ids and the counts.
Python · Starter code · orders.py
class Order:
"""An order numbered per prefix: KIT-1, KIT-2, LIB-1. The counts belong to the class."""
# Write the class's counts, __init__(self, prefix), issued(prefix) and reset(). The sample tests · test_orders.py
from orders import Order
def test_ids_count_per_prefix():
"""each prefix counts its own orders from 1"""
Order.reset()
ids = [Order("KIT").id, Order("KIT").id, Order("LIB").id, Order("KIT").id]
assert ids == ["KIT-1", "KIT-2", "LIB-1", "KIT-3"], f"got {ids}"
def test_each_order_keeps_its_id():
"""an order's id does not change when later orders are made"""
Order.reset()
first = Order("LIB")
second = Order("LIB")
assert (first.id, second.id) == ("LIB-1", "LIB-2"), f"got {first.id} and {second.id}"
def test_issued_is_read_from_the_class():
"""Order.issued(prefix) counts per prefix, 0 for an unused one"""
Order.reset()
Order("KIT")
Order("KIT")
Order("LIB")
counts = (Order.issued("KIT"), Order.issued("LIB"), Order.issued("CAF"))
assert counts == (2, 1, 0), f"expected (2, 1, 0), got {counts}"
def test_reset_starts_again():
"""after Order.reset() every prefix starts from 1"""
Order.reset()
Order("KIT")
Order.reset()
assert Order("KIT").id == "KIT-1"
assert Order.issued("KIT") == 1
def test_empty_prefix_is_refused():
"""an order needs a prefix"""
Order.reset()
try:
Order("")
except ValueError:
return
raise AssertionError('Order("") must raise ValueError') JavaScript · Starter code · orders.mjs
/** An order numbered per prefix: KIT-1, KIT-2, LIB-1. The counts belong to the class. */
export class Order {
// Write the class's counts, the constructor(prefix), static issued(prefix) and static reset().
} The sample tests · orders.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { Order } from './orders.mjs';
test('each prefix counts its own orders from 1', () => {
Order.reset();
const ids = [new Order('KIT').id, new Order('KIT').id, new Order('LIB').id, new Order('KIT').id];
assert.deepEqual(ids, ['KIT-1', 'KIT-2', 'LIB-1', 'KIT-3']);
});
test("an order's id does not change when later orders are made", () => {
Order.reset();
const first = new Order('LIB');
const second = new Order('LIB');
assert.deepEqual([first.id, second.id], ['LIB-1', 'LIB-2']);
});
test('Order.issued(prefix) counts per prefix, 0 for an unused one', () => {
Order.reset();
new Order('KIT');
new Order('KIT');
new Order('LIB');
assert.deepEqual([Order.issued('KIT'), Order.issued('LIB'), Order.issued('CAF')], [2, 1, 0]);
});
test('after Order.reset() every prefix starts from 1', () => {
Order.reset();
new Order('KIT');
Order.reset();
assert.equal(new Order('KIT').id, 'KIT-1');
assert.equal(Order.issued('KIT'), 1);
});
test('an order needs a prefix', () => {
Order.reset();
assert.throws(() => new Order(''), RangeError, "new Order('') must throw a RangeError");
}); A hint
Keep one dictionary for the whole class (JavaScript: a static Map), from each prefix to the last number it issued. The constructor reads the prefix's count with a default of 0, adds 1, stores it back and builds the id from it. issued and reset only touch that dictionary, so they need no self and are static.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
The questions cover every section, starting from the idea you have answered least well, and each right answer leads to a harder one:
Practice that adapts to you
Practise this lesson
10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · How does a static method differ from an instance method, and when do you write one?
An instance method is called on an object and receives it as its receiver, self or this, so it can read and
change that object’s fields: ticket.describe() reads that ticket’s seat. A static method belongs to the class and
receives no object, so it can use only its parameters and other static members: Ticket.label(2) builds “T-002”
from its argument alone. Call a static method through the class name. Use one when the method needs no object’s state,
such as a conversion, a validation or a named factory like Distance.from_kilometres(5). In Python there is a third
kind, the class method, which receives the class itself and so builds the right subclass when it is called on one.
Static methods cannot be overridden the way instance methods are; in Java a subclass’s static method of the same name
only hides the parent’s.
Three follow-ups to try, four answers each, with the reasoning shown for every answer:
Checkpoint
Interview round: three follow-up questions
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A Ticket class keeps a static counter and a static list of the tickets sold. Its tests pass one at a time and fail when the whole suite runs. Explain why, and redesign it so that the tests are independent while the program can still count every ticket.
-
Fresher to mid
When would you give a class named factory methods instead of, or as well as, public constructors? Use a Distance class built from metres or kilometres to show what factories add and what they cost.
Key takeaways
- A static member belongs to the class and exists once, like the ticket count, while each object holds its own instance fields, like the ticket’s number; ask whether the value can differ between two objects.
- A static method has no receiver, so it uses only its arguments and other static members; named factories such as
Distance.from_kilometres(5)are static methods that say what their number means. - Python’s class methods receive the class, so a factory called on a subclass builds the subclass, while a static method builds whatever class it names.
- A list or dictionary assigned in a Python class body is one object shared by every instance; create per-object
state in
__init__. - Static initialisation runs once per class, at a moment each language defines, and static state that changes leaks between tests, so keep such state in objects and keep static members for constants and pure functions.
Terms in this lesson
- class attribute
- In Python, a name assigned in the class body. It belongs to the class and is found through every object that has no attribute of its own by that name.
- class method
- In Python, a method marked @classmethod that receives the class itself as its first argument, cls, so a factory called on a subclass builds the subclass.
- named factory
- A static method that creates and returns an object, with a name that says what its arguments mean, such as Distance.from_kilometres(5) or Duration.ofMinutes(90).
- static member
- A field or method that belongs to the class itself, not to its objects: there is one copy for the whole program, and a static method receives no self or this.
References
- The Python Tutorial: Classes (class and instance variables) (Python Software Foundation)
- Built-in Functions: classmethod and staticmethod (Python Software Foundation)
- Learn Java: More on classes (understanding class members) (Oracle)
- The Java Language Specification, Java SE 25: 12.4.1 When initialization occurs (Oracle)
- The Java Language Specification, Java SE 25: 8.10 Record classes (Oracle)
- Static members (inline static data members, static member functions) (cppreference.com)
- TypeScript Handbook: Classes (static members and static blocks) (Microsoft)
- Static classes and static class members (C# programming guide) (Microsoft)
- TimeSpan.FromMinutes method (.NET API) (Microsoft)
- Kotlin: object declarations and companion objects (JetBrains)
- The Rust Reference: static items (The Rust Project)
- Dart: constructors (named constructors) (Dart (Google))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress