Learning Objectives
- Define and write instance methods that allow objects to perform actions using internal data.
- Use the self parameter inside instance methods to dynamically access and update object attributes.
- Implement the str magic method to customize user-friendly string outputs when objects are printed.
- Implement the repr magic method to provide clear, developer-focused representations for object debugging.
Giving Objects Life Beyond Simple Data
Imagine creating a video game character that has health points, but no way to take damage or heal on its own. Up until now, our custom objects have been passive data containers, but real-world objects need actions and behaviors to bring them to life.
The Problem with Passive Data
So far, you have learned how to store information inside objects using attributes. While storing data is essential, relying solely on static variables has major limitations:
- No self-control: Outside code has to manually change the internal state of your object.
- Repeated code: Every time you want to modify an attribute, you have to rewrite the same update logic in multiple places.
- High risk of bugs: It is easy to set an attribute to an invalid state (like setting a character's health to
-50) if there is no built-in logic to handle updates safely.
When your objects only hold data, they are like digital sticky notes they hold information, but they cannot do anything with it.
Connecting Behavior directly to Data
To build robust software, you need to connect what an object knows (its attributes) with what an object can do (its behaviors).
Instead of manipulating an object's variables manually from the outside, you can bundle functions directly inside the object.
# Passive Approach: External code manually changes raw variables
player_hp = 100
player_hp -= 20 # The external code has to handle the math manually
# Dynamic Approach: The object owns its data and controls its own actions
class Player:
def take_damage(self, amount):
self.hp -= amount
By shifting from passive data holders to interactive objects, your code becomes far more organized, readable, and resilient. In this section, you will learn how to write methods that allow your objects to dynamically update themselves and perform meaningful actions!
Why Functions Belong Inside Your Classes
Have you ever spent hours tracking down a bug, only to realize a random function in a completely different file altered your object's data? Bundling your logic directly inside the class that owns the data a concept known as encapsulation prevents broken application state and keeps your codebase clean.
When you write external functions to modify an object, your logic becomes scattered across your project. Any part of your program can reach in and change things, making your code fragile and hard to reason about.
# External function: Manipulates object state from the outside
def add_item_to_cart(cart, item):
cart.items.append(item)
cart.total_price += item.price
# Encapsulated behavior: The object manages its own state internally
class ShoppingCart:
def add_item(self, item):
self.items.append(item)
self.total_price += item.price
Replacing external state-manipulating functions with internal class methods gives you three major benefits:
- Single Source of Truth: All logic related to a specific object lives in one place, making updates and debugging straightforward.
- Cleaner Syntax: Calling
cart.add_item(item)is more intuitive than passing your object into an external function likeadd_item_to_cart(cart, item). - State Protection: The class controls how its own attributes are modified, preventing external code from accidentally corrupting data.
By shifting your mindset from passing data into external functions to telling objects to perform actions on themselves, your code becomes significantly easier to maintain.
The TV Remote Control
Imagine walking into your living room to watch a movie. You don't pry open the back of the TV set and manually rewire the circuit board just to turn up the sound; you simply press a button on your remote control.
In Object-Oriented Programming, instance methods act just like the buttons on a remote control, giving you a clean, safe interface to interact with an object's internal data.
When you press the volume_up button on your remote, the remote emits an infrared beam directed at one specific TV in front of you. It doesn't magically turn up the volume on your neighbor's TV across the street, nor does it turn up every TV in the world. The self parameter is that infrared beam it ensures your method call targets the exact object instance you are holding.
Here is a simple Markdown comparison to help you visualize how these real-world actions translate directly into Python code:
| Real-World TV Analogy | Python OOP Concept |
|---|---|
| The physical TV set sitting in your room | The specific object instance |
| The buttons on your remote control | Instance methods (e.g., volume_up()) |
| The infrared beam pointing at your TV | The self parameter |
| The current volume level or channel display | Attributes (e.g., self.volume) |
Let me show you how this looks in action using Python:
class Television:
def __init__(self, location):
self.location = location
self.volume = 10
def volume_up(self):
# self targets this specific TV unit's volume attribute
self.volume += 1
print(f"The {self.location} TV volume is now {self.volume}.")
# Creating two separate TV instances
living_room_tv = Television("Living Room")
bedroom_tv = Television("Bedroom")
# Pressing the button on the living room TV's remote
living_room_tv.volume_up()
living_room_tv.volume_up()
# Pressing the button on the bedroom TV's remote
bedroom_tv.volume_up()
If you run this code, you will get the following output:
The Living Room TV volume is now 11.
The Living Room TV volume is now 12.
The Bedroom TV volume is now 11.
Breaking Down the Mechanics
Let's look at how Python handles the target TV under the hood:
- Defining the button: When we write
def volume_up(self):, we are creating a new button for our remote control. The first parameter must beselfso the method knows which TV's internal data to modify. - Pressing the button: When you write
living_room_tv.volume_up(), you are using the remote. Notice that you don't manually pass anything inside the parentheses! - Targeting with
self: Python automatically takes the object on the left of the dot (living_room_tv) and passes it in asself. That's why modifyingself.volumeinside the method updates the living room TV to12while leaving the bedroom TV's volume untouched.
By treating methods as buttons and self as your targeting system, you keep your code clean, organized, and remarkably easy to debug.
Defining Instance Methods and Harnessing Self
If attributes are the raw data an object holds, instance methods are the actions it can perform using that data. Learning how to define instance methods and use self allows you to build dynamic objects that can read and modify their own internal state.
To define an instance method inside a class, you use the standard def keyword just like a regular function. However, there is one crucial rule: the very first parameter of an instance method must always be self.
When you call a method on an object using dot notation (like my_account.deposit(50)), Python automatically passes the instance itself into that self parameter behind the scenes. You never pass self manually when invoking the method.
The Code
Here is a practical script demonstrating how to define instance methods, read attributes, and update state. Run this code in your environment to see it in action:
The Output
When you run this script, Python produces the following output:
Alex's current balance is $100.
Deposited $50 into Alex's account.
Alex's current balance is $150.
A common beginner mistake is forgetting to include self as the first parameter in a method definition (for example, writing def deposit(amount):). If you leave self out, Python will throw a TypeError when you call my_account.deposit(50) because it automatically sends the object as the first argument, but your function definition isn't set up to receive it!
The Breakdown
Let's look at how the code works line-by-line:
def display_balance(self):: Defines an instance method. Notice thatselfis explicitly listed as the first parameter, telling Python that this method operates on an instance ofBankAccount.self.ownerandself.balance: Insidedisplay_balance(), usingself.gives us read access to the specific attribute values belonging tomy_account.def deposit(self, amount):: Takesselfplus an additional parameter,amount. When calling this method later, you only provide the value foramount.self.balance += amount: Updates the internal state of the instance directly. Any changes made toself.balancepersist on the object after the method finishes executing.my_account.deposit(50): Triggers thedepositmethod. Python automatically routesmy_accountintoselfand50intoamount.
Reading vs. Updating State
Using self inside a method lets you perform two primary operations on an instance:
| Operation | Syntax Inside Method | Practical Purpose |
|---|---|---|
| Read Attribute | self.attribute_name |
Access stored data to print, calculate, or inspect state. |
| Update Attribute | self.attribute_name = new_value |
Reassign or modify stored data to reflect changes in the object. |
By mastering this pattern, you give your objects the ability to manage their own data safely and predictably.
Customizing Object Display with str and repr
Have you ever tried printing a custom Python object only to get an unhelpful message like <__main__.Player object at 0x7f9b8c12a390>? Defining display magic methods allows you to replace confusing memory addresses with clear, readable text tailored for users and developers alike.
In Python, special methods flanked by double underscores (often called "dunder" methods or magic methods) let you customize built-in language behaviors. When it comes to displaying your objects as strings, Python relies on two primary magic methods: __str__ and __repr__.
__str__: Used for user-facing output. It provides a clean, easy-to-read string representation meant for end users when you callprint()orstr().__repr__: Used for developer inspection. It provides an unambiguous, detailed representation of the object ideally formatted like valid Python code used byrepr(), interactive shell consoles, and collection displays.
Here is how the two methods compare side-by-side:
| Feature | __str__ |
__repr__ |
|---|---|---|
| Primary Audience | End users | Developers & Debuggers |
| Triggered By | print(obj), str(obj) |
repr(obj), interactive console output, containers (lists/dicts) |
| Goal | Human readability | Unambiguous technical detail |

A common beginner mistake is implementing __str__ while completely forgetting __repr__. If you only define __str__, printing a container like a list of your objects (e.g., print([player1, player2])) will still display ugly memory addresses! However, if you define only __repr__, Python will intelligently use it as a fallback for __str__ calls as well.
Run this runnable code snippet to see how defining both methods changes how your custom objects behave:
The Output
Using print(): PixelKnight (Score: 1500)
Using repr(): Player(username='PixelKnight', score=1500)
Inside a list: [Player(username='PixelKnight', score=1500)]
The Breakdown
Let's break down how Python determines which output to display line-by-line:
def __str__(self) -> str:defines the string meant for end users. When you passplayer1directly toprint(), Python automatically invokes__str__behind the scenes to printPixelKnight (Score: 1500).def __repr__(self) -> str:defines the output for technical inspection. It returnsPlayer(username='PixelKnight', score=1500), matching the exact constructor pattern used to recreate the object.repr(player1)explicitly requests the technical representation ofplayer1, bypassing the__str__method completely.print("Inside a list:", team)shows why__repr__is critical. When objects live inside Python containers like lists, sets, or dictionaries, Python calls__repr__on each item so developers get complete inspection details while debugging collections.
The Two Faces of Python Objects
Every Python object you design has two distinct responsibilities: what it can do (its action interface) and how it presents itself (its representation interface). Understanding how to split these roles cleanly makes your classes intuitive to interact with and painless to debug.
Action Interface vs. Representation Interface
When you build custom classes, you will spend most of your time writing two types of methods:
- Action Interface: Custom instance methods that change attributes, execute logic, or trigger side effects (e.g.,
set_volume(),deposit(),attack()). - Representation Interface: Special magic methods (
__str__and__repr__) that return string descriptions of the object's current state without modifying it.
Let's look at a complete example separating these two responsibilities. Copy and run this code in your environment to see how Python interacts with each layer:
Output:
[Living Room] Volume updated to 75%
'Living Room' Speaker (Volume: 75%)
SmartSpeaker(device_name='Living Room', volume=75)
Line-by-Line Breakdown
speaker.set_volume(75): Calls our action interface. It modifiesself.volumeinside the object and prints an operational confirmation message.print(speaker): Triggers__str__(). Python automatically calls__str__when passing an object toprint(), returning a polished sentence meant for human end-users.print(repr(speaker)): Explicitly calls__repr__(). Therepr()function retrieves the developer representation, which explicitly shows the class name and argument values required to reconstruct the object.
Decision Framework: Choosing __str__ vs. __repr__
When designing your representation layer, deciding which magic method to implement depends on who is viewing the output. Use this practical decision framework:
| Feature / Criteria | __repr__ |
__str__ |
|---|---|---|
| Primary Audience | Developers, maintainers, and log files | End-users and human UI outputs |
| Main Goal | Unambiguous precision | Readability |
| Format Convention | Valid Python code string (e.g., SmartSpeaker('Office', 10)) |
Friendly formatted text (e.g., 'Office' Speaker at 10%) |
| Fallback Status | Default fallback used if __str__ is missing |
Skipped if missing (Python defaults to __repr__) |
Follow these three simple rules when building your classes:
- Always implement
__repr__first. Because Python falls back to__repr__when__str__is absent, defining__repr__ensures your object never displays as a cryptic memory address like<__main__.SmartSpeaker object at 0x7f...>. - Make
__repr__look like code. Aim to make the return string of__repr__match the exact syntax you would use to re-create the object in Python. - Keep actions and representations separate. Never modify internal attributes inside
__str__or__repr__. Their sole job is reporting current state, not altering it.
Recap: Methods and String Representations
You've taken a massive step forward in your Python journey by mastering how objects perform actions and present themselves. Understanding how instance methods interact with self and how custom string representations work gives you complete control over your custom classes.
Instance Method Execution Recap
Instance methods give your objects behavior, allowing them to process internal data and trigger actions:
- Instance methods are functions defined inside a class that operate on individual object instances.
- The
selfparameter acts as an explicit reference to the specific object instance calling the method, allowing you to access and update attributes dynamically. - When you invoke
my_object.method(), Python automatically passesmy_objectas the first argument intoselfbehind the scenes.
__str__ vs __repr__ Comparison Summary
Customizing how your objects display themselves makes your code easier to read and debug. Here is how the two string representation magic methods compare:
| Feature | __str__() |
__repr__() |
|---|---|---|
| Target Audience | End-users and non-technical viewers | Developers and maintainers |
| Primary Purpose | Readable, friendly, user-facing text | Unambiguous, detailed, developer-focused representation |
| Common Triggers | print(), str(), f-strings |
Interactive shell prompt, repr(), list/dict output |
| Idiomatic Output | Informal summary (e.g., "Alice (Admin)") |
Executable-style code or clear debug info (e.g., User(name='Alice', role='Admin')) |
| Fallback Behavior | Automatically falls back to __repr__() if not defined |
Falls back to Python's default ID memory address if missing |
By combining instance methods with clean __str__ and __repr__ implementations, your custom Python objects will behave just like built-in types. You are now fully equipped to build readable, maintainable, and interactive Python classes!