Object-Oriented Programming (OOP) Module 1 – Objects and classes
Classes, objects, fields and methods
How a class lists the fields each object holds and the methods all of them share, what self and this are, and one class written in 14 languages.
What you will learn
- Define a class with fields, a constructor and methods, and create two objects from it
- Distinguish what belongs to the class from what belongs to each object
- Explain what self and this refer to while a method runs
- Predict what each language does with a field that no constructor set
- Explain how struct and class differ in C++, C# and Swift
Before you start
What you’ll learn · Before you start
What you will learn
- Define a class with fields, a constructor and methods, and create two objects from it
- Distinguish what belongs to the class from what belongs to each object
- Explain what self and this refer to while a method runs
- Predict what each language does with a field that no constructor set
- Explain how struct and class differ in C++, C# and Swift
Before you start
On this page
Two cinema tickets cost 9.00 each, for seats C7 and C8. Upgrading the first to seat A1 must change that ticket and leave the other alone, and a class: A definition of a kind of object: the fields every object of that kind holds and the methods they all share. Ticket is a class; each ticket made from it is an instance. makes that the natural outcome: it lists the fields every ticket has and the methods every ticket can run, and each object made from it keeps values of its own.
Below, one class is written in 14 languages. You will see where each language keeps the fields and where it keeps the
methods, follow self and this through a call, and see what each language does with a field that nobody set.
A class describes the fields, and each object holds its own
The Ticket class below says two things about every ticket. It has a seat and a price, kept in cents so that 900
means 9.00, and it can describe itself and upgrade to another seat. No ticket exists until the program asks the
class for one: each call of the constructor: The code that runs when a new object is made and gives its fields their first values: __init__ in Python, constructor in JavaScript, a method named like the class in Java and C++, init in Swift. makes a new
instance: One object made from a class by its constructor. Instances share the class's methods and each holds its own values for the class's fields. with fields of its own. The program makes two tickets, upgrades one and describes
both:
Python 3.14 · main.py
class Ticket:
def __init__(self, seat: str, price: int) -> None:
# Each new object gets its own seat and price; the price is in cents.
self.seat = seat
self.price = price
def describe(self) -> str:
return f"seat {self.seat}, {self.price // 100}.{self.price % 100:02d}"
def upgrade(self, seat: str, extra: int) -> None:
self.seat = seat
self.price += extra
a = Ticket("C7", 900)
b = Ticket("C8", 900)
print(f"a: {a.describe()}")
print(f"b: {b.describe()}")
print("upgrade a to A1, 6.50 more")
a.upgrade("A1", 650)
print(f"a: {a.describe()}")
print(f"b: {b.describe()}") In Python: self is written out as the first parameter of every method, and a.describe() passes a as self. The fields come into being when __init__ assigns them; nothing declares them.
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 {
constructor(seat, price) {
// Each new object gets its own seat and price; the price is in cents.
this.seat = seat;
this.price = price;
}
describe() {
const cents = String(this.price % 100).padStart(2, "0");
return `seat ${this.seat}, ${Math.trunc(this.price / 100)}.${cents}`;
}
upgrade(seat, extra) {
this.seat = seat;
this.price += extra;
}
}
const a = new Ticket("C7", 900);
const b = new Ticket("C8", 900);
console.log(`a: ${a.describe()}`);
console.log(`b: ${b.describe()}`);
console.log("upgrade a to A1, 6.50 more");
a.upgrade("A1", 650);
console.log(`a: ${a.describe()}`);
console.log(`b: ${b.describe()}`); In JavaScript: The constructor runs when new Ticket(…) is evaluated, with this as the new object, and the fields appear when it assigns them.
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.Locale;
public class Main {
public static void main(String[] args) {
Locale.setDefault(Locale.ROOT);
Ticket a = new Ticket("C7", 900);
Ticket b = new Ticket("C8", 900);
System.out.println("a: " + a.describe());
System.out.println("b: " + b.describe());
System.out.println("upgrade a to A1, 6.50 more");
a.upgrade("A1", 650);
System.out.println("a: " + a.describe());
System.out.println("b: " + b.describe());
}
}
final class Ticket {
private String seat;
private int price;
Ticket(String seat, int price) {
// Each new object gets its own seat and price; the price is in cents.
this.seat = seat;
this.price = price;
}
String describe() {
return String.format("seat %s, %d.%02d", seat, price / 100, price % 100);
}
void upgrade(String seat, int extra) {
this.seat = seat;
price += extra;
}
} In Java: this.seat = seat is needed because the parameter seat hides the field of the same name. String.format runs after main sets Locale.ROOT, so the digits are the same in every locale.
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>
class Ticket {
public:
// Each new object gets its own seat and price; the price is in cents.
Ticket(std::string seat, int price) : seat_(std::move(seat)), price_(price) {}
std::string describe() const {
return std::format("seat {}, {}.{:02}", seat_, price_ / 100, price_ % 100);
}
void upgrade(std::string seat, int extra) {
seat_ = std::move(seat);
price_ += extra;
}
private:
std::string seat_;
int price_;
};
int main() {
Ticket a("C7", 900);
Ticket b("C8", 900);
std::println("a: {}", a.describe());
std::println("b: {}", b.describe());
std::println("upgrade a to A1, 6.50 more");
a.upgrade("A1", 650);
std::println("a: {}", a.describe());
std::println("b: {}", b.describe());
} In C++: The member initialiser list, : seat_(…), price_(…), sets the fields before the body of the constructor runs. describe() is const: it promises not to change the object.
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 a = new Ticket("C7", 900);
var b = new Ticket("C8", 900);
Console.WriteLine($"a: {a.Describe()}");
Console.WriteLine($"b: {b.Describe()}");
Console.WriteLine("upgrade a to A1, 6.50 more");
a.Upgrade("A1", 650);
Console.WriteLine($"a: {a.Describe()}");
Console.WriteLine($"b: {b.Describe()}");
// Each new object gets its own seat and price; the price is in cents.
sealed class Ticket(string seat, int price)
{
public string Seat { get; private set; } = seat;
public int Price { get; private set; } = price;
public string Describe() => $"seat {Seat}, {Price / 100}.{Price % 100:D2}";
public void Upgrade(string seat, int extra)
{
Seat = seat;
Price += extra;
}
} In C#: Ticket(string seat, int price) is a primary constructor, new in C# 12, whose parameters initialise the two properties. {Price % 100:D2} pads the cents to two digits.
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 ticket {
char seat[8];
int price;
};
static struct ticket ticket_new(const char *seat, int price) {
// Each new object gets its own seat and price; the price is in cents.
struct ticket ticket = {.price = price};
snprintf(ticket.seat, sizeof ticket.seat, "%s", seat);
return ticket;
}
static void ticket_describe(const struct ticket *ticket, char *out,
size_t size) {
snprintf(out, size, "seat %s, %d.%02d", ticket->seat, ticket->price / 100,
ticket->price % 100);
}
static void ticket_upgrade(struct ticket *ticket, const char *seat, int extra) {
snprintf(ticket->seat, sizeof ticket->seat, "%s", seat);
ticket->price += extra;
}
int main(void) {
char text[32];
struct ticket a = ticket_new("C7", 900);
struct ticket b = ticket_new("C8", 900);
ticket_describe(&a, text, sizeof text);
printf("a: %s\n", text);
ticket_describe(&b, text, sizeof text);
printf("b: %s\n", text);
puts("upgrade a to A1, 6.50 more");
ticket_upgrade(&a, "A1", 650);
ticket_describe(&a, text, sizeof text);
printf("a: %s\n", text);
ticket_describe(&b, text, sizeof text);
printf("b: %s\n", text);
return 0;
} In C: C has no methods. ticket_describe(&a, …) plays the method, and its first parameter is the object that the other languages pass for you as self or this.
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
{
// Each new object gets its own seat and price; the price is in cents.
public function __construct(
private string $seat,
private int $price,
) {}
public function describe(): string
{
$cents = $this->price % 100;
return sprintf('seat %s, %d.%02d', $this->seat, intdiv($this->price, 100), $cents);
}
public function upgrade(string $seat, int $extra): void
{
$this->seat = $seat;
$this->price += $extra;
}
}
$a = new Ticket('C7', 900);
$b = new Ticket('C8', 900);
echo "a: {$a->describe()}\n";
echo "b: {$b->describe()}\n";
echo "upgrade a to A1, 6.50 more\n";
$a->upgrade('A1', 650);
echo "a: {$a->describe()}\n";
echo "b: {$b->describe()}\n"; In PHP: Constructor promotion, private string $seat in the parameter list, declares the field and assigns it in one step.
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 Ticket {
seat: String,
price: u32,
}
impl Ticket {
fn new(seat: &str, price: u32) -> Self {
// Each new object gets its own seat and price; the price is in cents.
Ticket {
seat: seat.to_string(),
price,
}
}
fn describe(&self) -> String {
format!(
"seat {}, {}.{:02}",
self.seat,
self.price / 100,
self.price % 100
)
}
fn upgrade(&mut self, seat: &str, extra: u32) {
self.seat = seat.to_string();
self.price += extra;
}
}
fn main() {
let mut a = Ticket::new("C7", 900);
let b = Ticket::new("C8", 900);
println!("a: {}", a.describe());
println!("b: {}", b.describe());
println!("upgrade a to A1, 6.50 more");
a.upgrade("A1", 650);
println!("a: {}", a.describe());
println!("b: {}", b.describe());
} In Rust: &self is the explicit receiver, a borrowed ticket. upgrade takes &mut self, so a is declared mut and b is not. Ticket::new is a naming convention, not a keyword.
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 {
seat: string;
price: number;
constructor(seat: string, price: number) {
// Each new object gets its own seat and price; the price is in cents.
this.seat = seat;
this.price = price;
}
describe(): string {
const cents = String(this.price % 100).padStart(2, "0");
return `seat ${this.seat}, ${Math.trunc(this.price / 100)}.${cents}`;
}
upgrade(seat: string, extra: number): void {
this.seat = seat;
this.price += extra;
}
}
const a = new Ticket("C7", 900);
const b = new Ticket("C8", 900);
console.log(`a: ${a.describe()}`);
console.log(`b: ${b.describe()}`);
console.log("upgrade a to A1, 6.50 more");
a.upgrade("A1", 650);
console.log(`a: ${a.describe()}`);
console.log(`b: ${b.describe()}`); In TypeScript: The fields are declared first, and strict mode checks that the constructor gives each one a value. Parameter properties would do both at once, but they are not erasable syntax.
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 Ticket struct {
seat string
price int
}
func NewTicket(seat string, price int) *Ticket {
// Each new object gets its own seat and price; the price is in cents.
return &Ticket{seat: seat, price: price}
}
func (t *Ticket) Describe() string {
return fmt.Sprintf("seat %s, %d.%02d", t.seat, t.price/100, t.price%100)
}
func (t *Ticket) Upgrade(seat string, extra int) {
t.seat = seat
t.price += extra
}
func main() {
a := NewTicket("C7", 900)
b := NewTicket("C8", 900)
fmt.Println("a:", a.Describe())
fmt.Println("b:", b.Describe())
fmt.Println("upgrade a to A1, 6.50 more")
a.Upgrade("A1", 650)
fmt.Println("a:", a.Describe())
fmt.Println("b:", b.Describe())
} In Go: The receiver, (t *Ticket), is written before the method name. It is a pointer, so Upgrade changes the caller's ticket. NewTicket is Go's convention for a constructor.
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) var seat: String
private(set) var price: Int
init(seat: String, price: Int) {
// Each new object gets its own seat and price; the price is in cents.
self.seat = seat
self.price = price
}
func describe() -> String {
let cents = price % 100
return "seat \(seat), \(price / 100).\(cents < 10 ? "0" : "")\(cents)"
}
func upgrade(to seat: String, extra: Int) {
self.seat = seat
price += extra
}
}
let a = Ticket(seat: "C7", price: 900)
let b = Ticket(seat: "C8", price: 900)
print("a: \(a.describe())")
print("b: \(b.describe())")
print("upgrade a to A1, 6.50 more")
a.upgrade(to: "A1", extra: 650)
print("a: \(a.describe())")
print("b: \(b.describe())") In Swift: init must give every stored property a value before it returns, and the compiler checks it. Argument labels make the call read upgrade(to:extra:).
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 seat and its price, in cents.
class Ticket
def initialize(seat, price)
# Each new object gets its own seat and price; the price is in cents.
@seat = seat
@price = price
end
def describe
"seat #{@seat}, #{@price / 100}.#{(@price % 100).to_s.rjust(2, '0')}"
end
def upgrade(seat, extra)
@seat = seat
@price += extra
end
end
a = Ticket.new('C7', 900)
b = Ticket.new('C8', 900)
puts "a: #{a.describe}"
puts "b: #{b.describe}"
puts 'upgrade a to A1, 6.50 more'
a.upgrade('A1', 650)
puts "a: #{a.describe}"
puts "b: #{b.describe}" In Ruby: Instance variables such as @seat come into being when initialize assigns them, as Python's attributes do, and every method of the object can read them.
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
// Each new object gets its own seat and price; the price is in cents.
class Ticket(
seat: String,
price: Int,
) {
var seat = seat
private set
var price = price
private set
fun describe(): String {
val cents = (price % 100).toString().padStart(2, '0')
return "seat $seat, ${price / 100}.$cents"
}
fun upgrade(
seat: String,
extra: Int,
) {
this.seat = seat
price += extra
}
}
fun main() {
val a = Ticket("C7", 900)
val b = Ticket("C8", 900)
println("a: ${a.describe()}")
println("b: ${b.describe()}")
println("upgrade a to A1, 6.50 more")
a.upgrade("A1", 650)
println("a: ${a.describe()}")
println("b: ${b.describe()}")
} In Kotlin: The class header is the primary constructor. Each property starts from a parameter of the same name and keeps a private setter, so this.seat in upgrade means the property.
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 {
// Each new object gets its own seat and price; the price is in cents.
Ticket(this._seat, this._price);
String _seat;
int _price;
String describe() =>
'seat $_seat, ${_price ~/ 100}.${(_price % 100).toString().padLeft(2, '0')}';
void upgrade(String seat, int extra) {
_seat = seat;
_price += extra;
}
}
void main() {
final a = Ticket('C7', 900);
final b = Ticket('C8', 900);
print('a: ${a.describe()}');
print('b: ${b.describe()}');
print('upgrade a to A1, 6.50 more');
a.upgrade('A1', 650);
print('a: ${a.describe()}');
print('b: ${b.describe()}');
} In Dart: Ticket(this._seat, this._price) uses initializing formals, which store each argument in the field of the same name before the 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
a: seat C7, 9.00 b: seat C8, 9.00 upgrade a to A1, 6.50 more a: seat A1, 15.50 b: seat C8, 9.00
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 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
All 14 printed the same five lines. The last two are the point. upgrade ran on a, so a became seat A1 at 15.50,
while b kept seat C8 at 9.00. Both tickets ran the same describe and the same upgrade, written once in the class,
and each run read and changed the field: A named value stored in each object of a class, such as a ticket's seat and price. Also called an attribute, a member variable or a property, depending on the language. of one object only. That is the division of labour in
every class-based language: the class holds what all its objects share, the
method: A function defined in a class and called on one of its objects, such as ticket.describe(). It receives that object, as self or this, and can read and change its fields., and each object holds what is its own, the values of its fields.
The notes under the tabs show how much the syntax varies around that one idea. Python and Ruby create a field the
moment the constructor assigns it. TypeScript, Java, C#, C++, Kotlin, Swift and Dart declare the fields in the class,
and PHP declares them in the constructor’s parameter list. C has no methods at all and passes the struct to plain
functions, and Go and Rust write their methods outside the type, in a receiver and in an impl block.
Key idea
A class is written once and shared: its methods exist once for all its objects, while each object made from it holds its own values for the fields the class lists.
Checkpoint
Check: The class and its objects
self and this are the object a method was called on
When describe runs, how does it know which ticket’s seat to read? Every method receives the object it was called
on, its receiver: The object a method was called on, passed to the method as self or this. In a.describe(), the receiver is a.. Python makes the receiver a visible first parameter, self, and the call
a.describe() hands a to it. Step through one call on each ticket:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Two objects. The program made a and b from the same class. Each object holds its own seat and price and remembers its class; neither holds a copy of any method.
- Look in a first. a.describe() starts at the object. Its own fields are seat and price, and describe is not one of them, so the lookup goes on to its class.
- Found in the class. Ticket holds describe, once for every ticket. Python calls it with a as the first argument, so a.describe() is the same call as Ticket.describe(a).
- self is a. Inside the method, self is the object a. self.seat and self.price read a's fields, so the call returns "seat C7, 9.00".
- Same method, ticket b. b.describe() runs the very same method with self bound to b. It reads b's fields and returns "seat C8, 9.00". One method, two objects, two answers.
- upgrade changes self. a.upgrade("A1", 650) also runs with self bound to a, so it changes a's fields and nothing else. a is now seat A1 at 15.50, and b is still seat C8 at 9.00.
The Python tutorial states the rule exactly: the call x.f() “is exactly equivalent to MyClass.f(x)”. The next
program checks it and shows where Python keeps each part. Press Run, then change Ticket.describe(b) to
Ticket.describe(a) and run it again:
class Ticket:
def __init__(self, seat: str, price: int) -> None:
self.seat = seat
self.price = price
def describe(self) -> str:
return f"seat {self.seat}, {self.price // 100}.{self.price % 100:02d}"
a = Ticket("C7", 900)
b = Ticket("C8", 900)
# The object holds its fields; the class holds the method, once for every ticket.
print("a's own fields:", vars(a))
print("describe in a's fields?", "describe" in vars(a))
print("describe in Ticket?", "describe" in vars(Ticket))
# A call through the object is a call through the class with the object as self.
print(a.describe())
print(Ticket.describe(a))
print(Ticket.describe(b)) Output
a's own fields: {'seat': 'C7', 'price': 900}
describe in a's fields? False
describe in Ticket? True
seat C7, 9.00
seat C7, 9.00
seat C8, 9.00
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 where_methods_live.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
vars(a) lists only seat and price, because the method is not stored in the object. It lives in the class, and
Ticket.describe(a) gives exactly what a.describe() gave. The name self is a convention rather than a keyword, as
the tutorial says: Python passes the object in that first position whatever the parameter is called. Other languages
pass the receiver too, and differ only in whether you write it:
| Language | The receiver | Written in the method? |
|---|---|---|
| Python | self, the first parameter |
Yes, in every method |
| Rust | &self or &mut self |
Yes |
| Go | (t *Ticket), before the method name |
Yes |
| C | an ordinary first parameter, a pointer | Yes: the method is a plain function |
| C++23 | this const Ticket& self, the explicit object parameter |
Optional, new in C++23 |
| Java, C#, C++, JavaScript, TypeScript, Kotlin, Dart, PHP | this (PHP: $this) |
No, it is implicit |
| Swift, Ruby | self |
No, it is implicit |
The C++23 row comes from the proposal called deducing this: a method may now name its object as a first parameter
marked this, much as Python’s self is named, while the old implicit this keeps working.
Assigning to the parameter instead of the field
In Python, seat = seat inside __init__ assigns a local name to itself and gives the object nothing; the first
read of self.seat later raises AttributeError. Write self.seat = seat. Java has the same slip: in a
constructor, seat = seat; assigns the parameter to itself, and the field keeps its default, null.
Checkpoint
Check: self, this and the receiver
Every language decides what an unset field holds
A constructor’s job is to give every field of a new object a value. What happens when it forgets one depends on the language, and the answer decides whether the slip is caught before the program runs, when the field is first read, or never:
| Language | A field that the constructor never set |
|---|---|
| Python | Does not exist; reading it raises AttributeError |
| JavaScript | Reads as undefined, and arithmetic on it gives NaN |
| TypeScript | Compile error under strict: no initializer, not definitely assigned |
| Java | Gets its default: 0 for an int, null for a String |
| C# | Gets its default, 0 or null; with nullable checks on, a warning for a string |
| C++ | An int left out holds an indeterminate value; reading it is undefined behaviour |
| C | A local struct without an initializer holds indeterminate values; = {0} zeroes them |
| PHP | A typed property read before it is set throws an Error |
| Rust | Compile error: a struct literal must give every field |
| Go | Gets the zero value: 0, "" or nil |
| Swift | Compile error: an initializer must set every stored property |
| Ruby | Reads as nil |
| Kotlin | Compile error: the property must be initialized |
| Dart | Compile error for a field whose type does not allow null |
Three groups appear. Rust, Swift, Kotlin, Dart and TypeScript under strict refuse to compile the class. Python and
PHP fail at the first read of the field. Java, C#, Go, JavaScript and Ruby quietly supply a default, and C and C++ can
leave whatever the memory held. The quiet group is where the worst bugs live: a ticket whose price was never set costs
0 in Java or Go, and nothing anywhere says so. In those languages the constructor itself must check that every field
it sets makes sense, as the card’s methods checked the balance.
Java 25: a program without a class around it
JEP 512 lets a beginner’s file be just void main() { IO.println("Hello"); }. The compiler declares a class around
such a compact source file, a class that code cannot name, so its fields and methods still belong to a class.
Checkpoint
Check: Fields the constructor never set
struct and class mean different things in different languages
In C++ the keywords struct and class make the same kind of type. They differ only in the access you get without
asking for one: a struct starts its members and its base classes as public, a class starts them as private. Classes
in C++ are values, too: Ticket t("C7", 900); creates the object right there in the variable, with no new.
In C# and Swift the difference is much bigger. A struct is a value type, copied whole when it is assigned or passed to a function, while a class is a reference type, so assigning it gives a second name to the same object. Rust, Go and C have only structs: Rust and Go attach the methods from outside, and C passes the struct to functions.
| Language | struct |
class |
|---|---|---|
| C++ | Members public by default | Members private by default; otherwise identical |
| C# | A value type: copied on assignment | A reference type: shared on assignment |
| Swift | A value type: copied on assignment | A reference type: shared on assignment |
| Rust, Go | The type that holds fields; methods in impl blocks or receivers |
No such keyword |
| C | Fields only; functions take a pointer to it | No such keyword |
The C# and Swift rows matter as soon as a program says b = a: with a struct, b is a second ticket, and with a
class it is a second name for the first one. That difference is the subject of the next lesson.
Checkpoint
Check: struct and class across languages
Practise: a class for a mobile data plan
The exercise asks for a class of your own: a mobile plan with a price, a number of days and a daily data allowance, a constructor that refuses values that make no sense, two methods that compute from the fields and one that returns a new plan instead of changing the old one. Its sample tests run in your browser, in Python or JavaScript:
Exercise · Easy · Python, JavaScript
Write a class for a mobile data plan
Write the class Plan in plan.py (JavaScript: plan.mjs) for a prepaid mobile plan. A plan has three fields, all whole numbers: price in cents, days of validity and daily_mb, the data allowed each day in megabytes (JavaScript: price, days and dailyMb).
Plan(price, days, daily_mb)stores the three fields. If any of them is 0 or less, it raisesValueError(JavaScript:RangeError), so no plan object can exist with a price, a validity or an allowance that makes no sense.cost_per_day()(JavaScript:costPerDay()) returns the price per day in whole cents, rounded down:Plan(29900, 28, 1536).cost_per_day()is 1067.total_mb()(JavaScript:totalMb()) returns the data over the whole plan: 1536 × 28 = 43008 for that plan.extended(extra_days)(JavaScript:extended(extraDays)) returns a new plan,extra_dayslonger, whose price grows bycost_per_day()for each extra day; the plan it was called on does not change.extra_daysmust be at least 1.
The sample tests build plans, check every method on two plans, extend one and check that the original kept its fields, and try the values the constructor must refuse.
Python · Starter code · plan.py
class Plan:
"""A prepaid mobile plan: price in cents, days of validity and data per day in MB."""
# Write __init__(self, price, days, daily_mb), cost_per_day(), total_mb() and extended(extra_days). The sample tests · test_plan.py
from plan import Plan
def refused(make):
"""Whether calling make raises ValueError."""
try:
make()
except ValueError:
return True
return False
def test_fields_and_methods():
"""the constructor stores the fields; cost_per_day rounds down; total_mb multiplies"""
plan = Plan(29900, 28, 1536)
assert (plan.price, plan.days, plan.daily_mb) == (29900, 28, 1536)
assert plan.cost_per_day() == 1067, f"29900 // 28 is 1067, not {plan.cost_per_day()}"
assert plan.total_mb() == 43008, f"1536 MB a day for 28 days is 43008, not {plan.total_mb()}"
def test_each_plan_has_its_own_fields():
"""two plans made from the class answer from their own fields"""
small, large = Plan(15500, 30, 1024), Plan(71900, 84, 2048)
assert (small.cost_per_day(), large.cost_per_day()) == (516, 855)
assert (small.total_mb(), large.total_mb()) == (30720, 172032)
def test_extended_returns_a_new_plan():
"""extended(7) is a new plan 7 days longer; the original keeps its fields"""
plan = Plan(29900, 28, 1536)
longer = plan.extended(7)
assert longer is not plan, "extended must return a new Plan, not change this one"
assert (longer.price, longer.days, longer.daily_mb) == (37369, 35, 1536), "price 29900 + 7 * 1067, 35 days"
assert (plan.price, plan.days, plan.daily_mb) == (29900, 28, 1536), "the original plan changed"
def test_values_that_make_no_sense_are_refused():
"""a price, days or allowance of 0 or less, and extending by 0 days, raise ValueError"""
for args in [(0, 28, 1536), (29900, 0, 1536), (29900, 28, 0), (-100, 28, 1536)]:
assert refused(lambda: Plan(*args)), f"Plan{args} must raise ValueError"
assert refused(lambda: Plan(29900, 28, 1536).extended(0)), "extended(0) must raise ValueError" JavaScript · Starter code · plan.mjs
/** A prepaid mobile plan: price in cents, days of validity and data per day in MB. */
export class Plan {
// Write constructor(price, days, dailyMb), costPerDay(), totalMb() and extended(extraDays).
} The sample tests · plan.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { Plan } from './plan.mjs';
test('the constructor stores the fields; costPerDay rounds down; totalMb multiplies', () => {
const plan = new Plan(29900, 28, 1536);
assert.deepEqual([plan.price, plan.days, plan.dailyMb], [29900, 28, 1536]);
assert.equal(plan.costPerDay(), 1067, '29900 / 28 rounded down is 1067');
assert.equal(plan.totalMb(), 43008, '1536 MB a day for 28 days is 43008');
});
test('two plans made from the class answer from their own fields', () => {
const small = new Plan(15500, 30, 1024);
const large = new Plan(71900, 84, 2048);
assert.deepEqual([small.costPerDay(), large.costPerDay()], [516, 855]);
assert.deepEqual([small.totalMb(), large.totalMb()], [30720, 172032]);
});
test('extended(7) is a new plan 7 days longer; the original keeps its fields', () => {
const plan = new Plan(29900, 28, 1536);
const longer = plan.extended(7);
assert.ok(longer !== plan, 'extended must return a new Plan, not change this one');
assert.deepEqual([longer.price, longer.days, longer.dailyMb], [37369, 35, 1536], 'price 29900 + 7 * 1067, 35 days');
assert.deepEqual([plan.price, plan.days, plan.dailyMb], [29900, 28, 1536], 'the original plan changed');
});
test('a price, days or allowance of 0 or less, and extending by 0 days, throw a RangeError', () => {
for (const args of [[0, 28, 1536], [29900, 0, 1536], [29900, 28, 0], [-100, 28, 1536]]) {
assert.throws(() => new Plan(...args), RangeError, `new Plan(${args}) must throw a RangeError`);
}
assert.throws(() => new Plan(29900, 28, 1536).extended(0), RangeError, 'extended(0) must throw a RangeError');
}); A hint
Check the three arguments at the top of the constructor and raise before storing anything. In extended, build the answer with the class itself, Plan(…) (JavaScript: new Plan(…)), from the fields of self (this), instead of assigning to them: a method that returns a new object leaves its own object alone.
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.
Now check yourself on the whole lesson. The questions follow your answers: a weak idea comes back first, and a right answer brings a harder question:
Practice that adapts to you
Practise this lesson
8 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What is the difference between a class and an object?
A class is a definition and an object is a thing made from it. The class lists the fields every object of its kind
will hold and defines the methods they all share; it is written once. An object, an instance of the class, is created
at run time by the constructor and holds its own values for those fields. In the lesson’s program, Ticket is the
class, and a and b are two objects: both run the same describe and upgrade code, stored once in the class, yet
upgrading a to seat A1 left b at seat C8 and 9.00. A useful test in an answer: methods belong to the class, field
values belong to each object, and a method finds its object through self or this.
Interviewers rarely stop there. Here are three follow-ups to try, with four answers each; the feedback explains what each answer covers:
Checkpoint
Interview round: three follow-up questions
Key takeaways
- A class lists the fields of its objects and defines their methods once; each object made by its constructor holds
its own field values, so upgrading ticket
aleft ticketbunchanged. - A method receives the object it was called on: Python and Rust name it in the parameter list, Go and C pass it
explicitly, and Java, C#, JavaScript and most others pass it as an implicit
thisorself. - In Python,
a.describe()is the callTicket.describe(a), andvars(a)shows only the fields, because the method lives in the class. - A field the constructor forgets is a compile error in Rust, Swift, Kotlin, Dart and strict TypeScript, an error on first read in Python and PHP, and a silent default or worse in Java, C#, Go, JavaScript, Ruby, C and C++.
structandclassdiffer only in default access in C++, but in C# and Swift a struct is copied on assignment while a class is shared.
Terms in this lesson
- class
- A definition of a kind of object: the fields every object of that kind holds and the methods they all share. Ticket is a class; each ticket made from it is an instance.
- constructor
- The code that runs when a new object is made and gives its fields their first values: __init__ in Python, constructor in JavaScript, a method named like the class in Java and C++, init in Swift.
- field
- A named value stored in each object of a class, such as a ticket's seat and price. Also called an attribute, a member variable or a property, depending on the language.
- instance
- One object made from a class by its constructor. Instances share the class's methods and each holds its own values for the class's fields.
- method
- A function defined in a class and called on one of its objects, such as ticket.describe(). It receives that object, as self or this, and can read and change its fields.
- receiver
- The object a method was called on, passed to the method as self or this. In a.describe(), the receiver is a.
References
- The Python Tutorial: Classes (class objects, instance objects, method objects) (Python Software Foundation)
- Built-in Exceptions: AttributeError (Python Software Foundation)
- Learn Java: Creating classes (Oracle)
- The Java Language Specification, Java SE 25: 4.12.5 Initial values of variables (Oracle)
- JEP 512: Compact Source Files and Instance Main Methods (OpenJDK)
- TypeScript Handbook: Classes (fields and strictPropertyInitialization) (Microsoft)
- P0847R7: Deducing this (ISO/IEC JTC1/SC22/WG21 (the C++ committee))
- Access specifiers (default access of class and struct) (cppreference.com)
- Default-initialization (indeterminate values) (cppreference.com)
- Value types (C# reference) (Microsoft)
- Default values of C# types (Microsoft)
- The Swift Programming Language: Structures and Classes (Swift.org)
- The Swift Programming Language: Initialization (Swift.org)
- Rust error code E0063: a struct literal is missing a field (The Rust Project)
- The Go Programming Language Specification: the zero value (The Go Authors)
- Kotlin: properties (JetBrains)
- Dart: sound null safety (Dart (Google))
- PHP manual: properties (typed properties) (The PHP Group)
- Ruby documentation: assignment (instance variables) (Ruby)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress