Python classes in Rust (Basic)
Context? Yes, please!
In this article, we're doing Python classes in Rust. You can find the previous article here
I didn't intend to write this article so soon following the last one, but the last one was such a big hit with me... evidenced by the fact that I couldn't stop thinking about what more I could learn about Rust, and wondering at what point I'd start feeling the drag of its infamous learning curve. Thus far, everything's making logical sense... which is honestly the best kind of sense made 'cause I keep having these moments where I go, "uhhh... but wait, how about [thing] and how does it relate to [other_thing]?" and then "why tf would they do that?!" followed by, "Ohhh yeahhh so that's whyy" *snorrt boyy am I loving it!
Today's lesson is brought by myself and Chat, sparked by a random thought I had: 'Is Rust an OOP lang'? That's a dump question. But there's no such thing as a 'dumb questions'. Anyway, I did get this answer:
Chat: Rust is not an object-oriented language in the traditional sense, although it supports some OOP ideas. The primary paradigm is multi-paradigm, with a strong emphasis on: Generic programming, Functional programming concepts, Systems programming, Data-oriented design, Trait-based polymorphism. The biggest difference is that Rust replaces class inheritance with traits and composition... [insert word soup]
Yeah, blah blah blah... That response does not satisfy. I had to take a minute to really consider what I meant by 'OOP', and at the risk of getting lip-whipped by certain factions of the online software community, I realized what I really meant was, 'Does Rust implement classes?' The answer is no. Rust is a systems language that borrows heavy inspiration from functional programming. It does have some object-oriented concepts, like encapsulation and polymorphism, but it absolutely does not have inheritance... a concept it entirely replaces with composition.
That sounds interesting and all, so let's dig in.
Inheritance?? Do I have to pay taxes on that?
Depends... but before talking about Rust's Python class equivalent, there are two concepts we really need to grok about 'code-anizational' philosophies. (literally just made that up)
code-anization (adj.): Describing the structural, architectural, and organizational qualities of a software system of codebase. Formerly: 'Code Organization'. Example Usage: "Didn't yo mama teach you nothing about code-anization?
code-anizational philosophy (adj.): The underlying principles a programming language encourages for structuring, grouping, and relating code constructs such as data, behaviour, modules, namespaces, and abstractions.
As software grows big and complex, there are inevitably going to be many parts of the program that need to do the same thing and process the same (or similar) data over and over and over. Rather than rewriting the exact same piece of code in a dozen different files (or modules), programming languages are designed with a preferred mental model for organizing that code.
Python's philosophy is extremely object-centric and encourages (but doesn't strictly enforce) a code-anizational philosophy1 called Inheritance. Rust's preferred approach is more about separating data from behavior using a code-anizational philosophy called Composition.
Let's look at a few formal definitions to build our mental model:
A Class: is very simply, a blueprint or template that groups related data (attributes) together with the behaviour (functions) that operates on that data
An Object: is an actual, living instance of a class, containing it's own isolated copy of that class's data and behaviour
Mental model: If you're literally picturing an architectural blueprint or schema (the class) vs. a physical house (the object) built from that blueprint... you are exactly right! Moving on...
Inheritance: is an Object-Oriented Programming feature that lets one class (the Child) reuse the data and behavior of another class (the Parent), while also adding or changing its own unique data and behavior.
Composition: is pattern of building a complex object by giving it other smaller objects as constituent parts (think Lego blocks). Each part is responsible for its own behavior, allowing the larger object to reuse their functionality.
Simply put, Inheritance and Composition are just different opinions (bite me) on how you should reuse and combine swaths of code so you don't repeat yourself all over... [see: DRY Principle]
Remember this: Code-anization and reuse is really about how you model the relationship between objects. I'll explain this later, trust me. Just hang on to that thought for a second.
But wait... what's Behaviours? well, you see... In programming, a class defines both the data it stores and the behavior it provides. Behavior is expressed through functions, which operate on the class's data. Ergo, a behavior is simply what a named piece of software can do, while data is what the program knows.
Now, let's look at some actual examples of inheritance and composition with Python classes and talk about what I meant by "object relationship modeling."
Starting with Inheritance... (The "IS-A" Relationship)
Generally, when programmers need to reuse the functionality of an existing class, the OOP paradigm encourages them to have the new class (the child) quite literally INHERIT (or sayy COPY) the entire data and behavior of the existing target class (the parent).
When a child class inherits from a parent class, it possesses all of the parent's traits (alot like humans yeah?). For all logical purposes, the child class IS the parent class. This is an object relationship modeling pattern where Class B IS-A Class A.
Take a looky here: Imagine we are building a game with different types of enemies, and we want all of them to be able to take damage. If we wrote this without code reuse, it would look like this:
class ZombieMan:
def groan(self):
return "Braaaaains..."
def take_damage(self):
return "Ouch... Lost 5 hp."
class ZombiePlant:
def release_spore(self):
return "**poison puff puff**"
def take_damage(self):
return "Ouch... Lost 5 hp.""
That works fine for simple code, but as we add more enemies, we'd have to keep typing out the take_damage() function for every single one... and just no. Inheritance lets us simply do:
# Shared trait
class Enemy:
def take_damage(self):
return "Ouch... Lost 5 hp."
# INHERITANCE: ZombieMan can 'take damage'
class ZombieMan(Enemy):
def groan(self):
return "Braaaaains..."
# INHERITANCE: ZombiePlant can 'take damage'
class ZombiePlant(Enemy):
def release_spore(self):
return "**poison puff puff**"
Now look at that.... instead of repeatedly typing take_damage() every time we dream up a new bad guy, we create a base class for that shared behavior. For all intents and purposes, the ZombieMan IS AN Enemy.
Inheritance initially seemed like the greatest invention since the compiler because it allowed us to write significantly less code. However, over time, it became clear that inheritance could easily be abused. You end up in scenarios where Class D inherits from Class C, which inherits from Class B, which inherits from Class A... and it could get even worse. These types of inheritance chains makes the code so fragile and difficult to reason about that programmers literally invented the term inheritance hell.
And now, Composition... (The "HAS-A" Relationship)
Really smart people figured out Composition as a robust alternative to Inheritance where larger objects are built by literally holding other objects and delegating tasks to them. These smaller objects are fully responsible for their own behaviors.
Instead of an "IS-A" relationship, composed objects are built on a "HAS-A" relationship.
Think about a Player holding a Sword. A Player is not a Sword. A Player has a Sword. If you try to use inheritance here, your mental model breaks. So, instead, you'd wanna build the Sword on its own, and then give it to the Player:
class Boomerang:
def __init__(self, damage: int):
self.damage = damage
def throw(self):
return f"Swoosh! +{self.damage} damage."
class Sword:
def __init__(self, damage: int):
self.damage = damage
def swing(self):
return f"Swish! +{self.damage} damage."
class Player:
def __init__(self):
# COMPOSITION: The Player 'has a' weapon.
self.weapon = Sword(damage=15)
def attack(self):
# Player delegates the actual attacking behavior to the weapon object
return self.weapon.swing()
In this logic, the Player is just a container delegating the attack task to whatever weapon it currently holds.
With composition, swapping behaviors is effortless. We can just change the component under the hood: self.weapon = Boomerang(damage=15). The Player is still a Player, but just holding something new. If we forced Inheritance here (making the Player inherit from a Sword class), the player would be permanently, structurally hard-coded as a sword-swinging entity. To give them a boomerang, we'd literally have to destroy the Player object and instantiate a completely new class.
And that is the fundamental difference. Inheritance says "I am this," while composition says "I hold this." Aaaand that's that relationship modelling between objects I mentioned earlier. That's all of what code-anization is about.
Full Throttle Rust... heeave hoo!
Swinging back... Rust embraces the concept of composition so completely that it does not support inheritance at all. In Python, a Class bundles data (attributes) and behavior (functions) together into one giant, monolithic object meatpie (I'm so sorry Mr Rossum). Rust hates that and prefers its ingredients served on completely separate plates.
To make this work, Rust splits the traditional class into three core concepts:
- The
struct(Data Layout): (short for structure), is a user-defined type that groups related pieces of data under a single name. It very similar to Python's@dataclassin that it's main job is to hold data and define the shape (type) of the data it holds. Take a look:
# A Python dataclass defining type named Duck
# holding data `name` shaped/of type String
@dataclass
class Duck:
name: str
is very similar to:
// A Rust struct defing a `Duck` type
// with a field called `name` with the shape of String
struct Duck {
name: String,
}
The only exception being that while you can define functions (behaviours) inside a Python dataclass... Functions absolutely do NOT go inside a Rust struct. Rust seperates data from behaviour, which leads us directly to...
- The
implblock (Behaviour Implementation): (short for implementation), animplblock is where you define the behaviour of a type via functions... uhm, that's it. for example:
// implementing a `quack` behaviour for the `Duck` type
impl Duck {
fn quack(&self) {
println!("Quack!");
}
}
That's literally all an impl block is for: separating the type from the implementation of behaviours.
Taking the opportunity to play language designer... if we were to capture the spirit of Rust in Python, we'd have something like this
class Duck:
# here's the data layout for this type
def __init__(self, name):
self.name = name
implement Duck:
# here's the method for `Duck` class
def quack(self):
...
This isn't legal Python, but I've mangled it to demonstrate how the Rust philosophy is really only about separating data layout of a type (via Struct) from the functions operating on that type's data (via impl).
What's Type? A type, short for datatype, defines what a value is, what kind of data it can hold, and what operations can be performed on it2. This covers built-in (primitive) types like
integers,strings,vectorsas well as user-defined types like ourDuckstruct. The Pythonclassand RustEnumandStructkeywords are all ways of defining literal custom datatypes. 'Type' is a word that gets thrown around alot. You should know what it means.
Translating Our Weapon Arsenal
We've discussed two of the three concepts Rust uses to replace the traditional Python class. We'll get to the third in a minute, but first, let's translate our Python Sword type from earlier into Rust:
// -- The Struct (Holding Data) --
struct Sword {
damage: i32, // The Data
}
// -- The Impl block (Defining Behavior) --
impl Sword {
fn swing(&self) -> String {
format!("Swish! +{} damage.", self.damage)
}
}
Notice how clean that separation is compared to the Python class equivalent where what the object knows and what the object does sit under the exact same indentation block?
class Sword:
# -- THE DATA (What the object KNOWS) --
def __init__(self, damage: int):
self.damage = damage
# -- THE BEHAVIOR (What the object DOES) --
def swing(self):
return f"Swish! +{self.damage} damage."
So, we've got our Sword. Say we want to define a Player type to use this weapon, we simply define a Player struct that physically holds the Sword struct inside it... like this:
struct Player {
// COMPOSITION: The Player 'has a' Sword
weapon: Sword,
}
impl Player {
fn attack(&self) -> String {
// Delegating the behavior to the composed object
self.weapon.swing()
}
}
And just like that, we've achieved the exact same 'HAS-A' compositions we did in Python! The Player struct is holding an instance of Sword.
But of course... there's a problem
In our Python example, swapping the Sword object for Boomerang was as easy as writing self.weapon = Boomerang(...). Because Python is dynamically typed, meaning it determines the type of a value at runtime, it didn't care what object was assigned to weapon, so long as that object possessed a .swing() or .throw() method when it came time to attack.
But Rust is statically typed. It needs to know the exact type of every single value before the program even runs. And look at what we did to our Player struct:
struct Player {
weapon: Sword, // Hardcoded type!
}
We've basically structurally hardcoded the weapon field to be exactly the shape of a Sword. It is contractually obligated to hold a Sword and only a Sword. If we designed a Boomerang type and handed that to the Player, the compiler is going to have an absolute hissy fit.
We need a way to tell the compiler: 'Look, I don't care what specific type of value is handed to the Player, as long as it behaves like a weapon.'
Enter: Traits (The Contract)
If a struct is the data, and an impl block is the behaviour, a trait is a contract. It's the third and final concept in Rust's answer to classes. A trait in Rust is simply a definition of shared behavior.3
Shared behaviour huh? Let's think this through... We've got a Sword type and we're about to define a Boomerang type. To the Player, these are both weapons, and it needs to be able to pick up either one of them up effortlessly.
To do this, we're gonna have to consider what makes a weapon a weapon. I'd sayy it's a weapon if it hurts... so a weapon needs to be able to deal damage. We don't care how it does it, only that it can.
That shared capability is our trait. We can define the contract like so:
// The Contract (Trait)
trait Weapon {
// We don't define HOW to do it here, just that it MUST be doable.
fn deal_damage(&self) -> String;
}
A trait doesn't provide the code implementation. It just declares a mandatory function signature (what goes in and what comes out). Now, we can explicitly implement this trait for our weapons:
struct Sword { damage: i32 }
struct Boomerang { damage: i32 }
// The Sword type implements the Weapon trait
impl Weapon for Sword {
fn deal_damage(&self) -> String {
format!("Swish! +{} damage.", self.damage)
}
}
// The Boomerang type implements the Weapon trait
impl Weapon for Boomerang {
fn deal_damage(&self) -> String {
format!("Swoosh! +{} damage from afar.", self.damage)
}
}
SWEET!... geddit? You can read and reason about it EXACTLY like it looks in ENGLISH. impl Weapon for Sword means 'implement the Weapon contract for the Sword struct'. So any struct implementing the Weapon contract must have a function signature matching deal_damage
Conceptually, it's extremely similer to Python's typing.Protocol, which allows us to define a structurally similar function contract that static type checkers can enforce. You'd se something like:
from typing import Protocol
from dataclasses import dataclass
# The Contract
class Weapon(Protocol):
def deal_damage(self) -> str:
... # An ellipsis means "I don't care how, just match this signature."
@dataclass
class Sword:
damage: int
def deal_damage(self) -> str:
return f"Swish! +{self.damage} damage."
The massive difference here is explicitness. In Python, Sword implicitly satisfies the Weapon protocol just by having a matching method name because If it walks like a duck and quacks like a duck, Python says it's a duck.
Rust doesn't play that. In Rust, even if your Sword has a deal_damage function, the compiler will reject it unless you explicitly state your allegiance by writing impl Weapon for Sword.
A concept called Generics
Now that both Sword and Boomerang guarantee they implement the Weapon trait, we can upgrade our Player struct to accept ANY type that satisfies our contract (rather than a specific type). We do this using Generics:
// Notice the <W: Weapon> part!
struct Player<W: Weapon> {
weapon: W,
}
impl<W: Weapon> Player<W> {
fn attack(&self) -> String {
// safe to call, because W is guaranteed to be a Weapon!
self.weapon.deal_damage()
}
}
Quick Detour: What the hell are Generics? Simply put, Generics are a language feature that lets us write code that works with many different types without having to rewrite the same exact logic for each one. Whenever you see that angle bracket
<T>or<T: ...>syntax in Rust, it is passing compile-time type information. Think of it as the compiler asking, 'What kind of type is this structure allowed to work with?'
Now, I know that <W: Weapon> syntax looks scary, but it's just a generic type definition4. It translates to, 'Let W represent any type that implements the Weapon trait'. Pretty neat huh?
But hold on... we still need to understand this line: impl<W: Weapon> Player<W>, why W is repeated so much, why we have <W: Weapon> in the struct and again in the impl line.
I'd sayy a good to think about it is to realize that struct Player<W: Weapon> only establishes the blueprint's rules, while the impl line is the actual behavioural application. So like when we define the struct:
struct Player<W: Weapon> {
weapon: W,
}
It's not like we're just 'typehinting' or ONLY just declaring the shape of the field. We're also telling Rust that to even build a Player type in the first place, it must have received a generic type W, and that type MUST have signed the Weapon contract (i.e. the Weapon trait).
Then, when it comes time to actually give the Player it's behaviours (via impl), we have to do a two-step dance to declare the generic parameter required by Player before we actually apply it to Player hence impl<W: Weapon> Player<W>. Breaking it up:
impl<W: Weapon>(The Declaration): In Rust, we cannot use a generic variableWwithout defining it first. This first chunk creates the generic type variableWfor the scope of this specificimplblock and immediately applies a trait bound on it (meaning it MUST implement theWeapontrait)5. If we skipped this and just wroteimpl Player<W>, Rust would throw an error essentially saying, 'What the hell is a W? Is that a struct? I've never heard of that type!' because we neglected to declare it.Player<W>(The Type Application): This second chunk is where we specify the exact struct we're targeting. It tells Rust to, 'Attach these functions to thePlayerstruct, specifically when it is holding the generic typeWthat I just declared.'
Basically:
impl<W: Weapon>(create generic variableWimplementingWeapontraits) --apply-to-->Player<W>(which already was defined as a type expecting a generic variable implementingWeapontrait i.e.struct Player<W: Weapon>)
If we brought our Python Player equivalent into the mix using our Protocol, it would look like this:
@dataclass
class Player:
# The type hint demands ANY object that fits the Weapon Protocol
weapon: Weapon
def attack(self) -> str:
return self.weapon.deal_damage()
If we tried to pass a garbage object (one that doesn't follow the contract) into Python's Player, the IDE's type-checker would throw a massive red squiggly line... but the Python script would still attempt to run. The Rust version wouldn't even compile.
Time to say goodbye... for now
At this point, it's pretty clear how Rust’s entire language design is built to encourage Composition as the default pattern of code-anization. By separating data layouts (struct) from behavioral execution (impl), and governing their interactions through explicit contracts (traits), Rust completely sidesteps the need for inheritance whilst exposing you to a much much more powerful and flexible code reuse and organizational philosophy.
There isn't even a chance to stumble into inheritance hell... there's no way to even do inheritance. And you can't build a fragile tower of child classes because there are no classes to stack. You are forced to think in terms of modular, swappable components from day one.
And yeah, it's a bit difficult to stop seeing classes everywhere when you're first constructing objects in Rust. You just have to try to think about your objects like Lego blocks. That's really the view Rust is encouraging, and for your troubles, you are rewarded with a very different kind of flexibility.
For example, you can have multiple impl blocks for a single type, even spread across entirely different files, and Rust will seamlessly treat them like one gigantic monolithic impl block. I know, right... Gawd class who? Add in the fact that shared traits are strictly enforced, and the compiler is actually doing 25% of the architectural thinking for us, alongside the IDE doing another 5% by checking syntax correctness.
Coming from Python, you're probably used to not having to think about types at first (a very important qualifier). It feels great being able to simply switch types midway, or self-guarantee what a type will look like at runtime even if it isn't explicitly defined that way in your code. That might seem like flexibility, oh and it is... at a certain stage of the workflow, but when your codebase is all grown up, I'd bet there are times you just wish Python would do the type tracing and correctness checks for you. That way, you wouldn't have to simultaneously juggle type correctness and logic correctness in your head. *cries

Anyway, it took me almost a week and half a bottle of red to get here. It wasn't exactly that any of the concepts presented here were difficult to grok... in fact, they made too much sense to me. So much so, I thought I'd be able to write up everything I'd figured out in two days. lol yeahh riiiight...
My intent was to write an article explaining foreign concepts requiring as few prerequisites from a halfway competent Python junior as possible, all while keeping the content dense but approachable without needing to Google terms. I hope I succeeded at that because I had to let go of, and relegate to another article, explaining a bunch of fun but complex quirks and features
Anyway, there's still a lot we have to learn about Rust's three musketeers, how to use them, and what they're truly capable of. I definitely want to expand on generics and object instantiation (did you notice I didn't actually instantiate any objects here?). We still need to answer questions like:
If Rust has no classes and isn't exactly object-oriented, how does it have objects?
How do we build constructors for an object in Rust? Is the term 'object' correct?
What the hell is &self? Ohkay 'self' is familiar but &?
You're probably still weirded out by <...> syntax?
It took a shit ton of willpower to self-regulate, tbh I'm prolly greenlantern material, and avoid info-dumping... so next time, we'll dive deeper into these concepts and even touch on Rust's famous Borrow checker
dum dum dummmmm 😱
Go get some coffee. I'll get started on the next draft, and the next time we meet up, I'll take you on a Rust-inspired joyride.
All mistakes are mine. See ya around, cuh!
Footnotes
-
I really said that with a straight face lol ↩
-
Even better... a type defines what a value is and what can be done with it ↩
-
In english, 'trait' is defined as 'a distinguishing quality or characteristic of someone or something'. That definition literally applies here. ↩
-
In english, 'generic' can be defined as 'something broad, common, or not tied to a specific identity or brand.' This applies wholesale here. ↩
-
A trait bound is a requirement that a type MUST implement certain traits in order to be used by a generic type or function. ↩