Article.introduction
If you’ve ever had to google “What’s the difference between an attribute and a property in Python?”, well…you’re not alone—I’ve been there, squinting at the screen, flipping through docs, trying to figure out exactly why some variables are called attributes, why others are called properties, and I haven’t found a source that communicates some of the vital points of these in a way that resonated with me.
After some training research and experimenting of my own, I have developed a much better understanding than I had. In this post, I’ll try my best to clear this up as I understand it. We’ll see where attributes live, how properties act like attributes but pack a little extra punch, and how to create them in Python.
Attributes in Python
In Python, when we talk about attributes we usually mean a value attached to an object. Think “A variable that belongs to either a Class or an instance of a class.
class Car:
wheel_count = "4" #This is a class attribute any class instanciated is going to have 4 wheels
def __init__(self, make, model):
self.make = make #This is an instance attribute
self.model = model
dreamcar_1 = Car("Porsche", "911")
prit(dreamcar_1.make) #"Porsche"
In this example:
We have wheel_count (conventionally we would want this to be an integer if we wanted to ever use it to do math but for now the sake of example we will leave it as a string) which is a Class Attribute. Im gonna go out on a limb here and say that we can assume most cars have 4 wheels. With that being said, every Car we instantiate will by default have 4 wheels.
We also have make and model, not every Car is a Porsche 911 (although that’d be pretty cool). What we CAN take from that is that every Porsche 911 is a Car. This means make and model are going to be our Instance attributes.
So in short: All Cars have 4 wheels, not all cars are the same make and model.
Properties: Like Attributes…But With Superpowers
So what’s a Property? At first glance you’d assume it’s just a misnamed attribute. You say my_object.property_name, and you get (or set, but we’ll get there) a value. The key difference here is that a property lets you run methods() behind the scenes to get, set, or delete that value.
So:
An attribute is basically a normal variable that is attached to your class.
A property is an attribute that has extra logic when you access or modify it.
So then why would I want to use a property?
Properties are great for encapsulation—when you want the rest of your code to treat something like a simple attribute, but under the hood(or trunk if were still on the 911 thing), you might want to:
Validate data (Make sure a name is a string that is greater than 0 letters)
Transform data on the fly (Make that same string upper case)
Log or trigger some sort of action whenever a value is changed
Creating a Property With @property
A common way to create and manage data behind the scenes is through the @property
decorator. It lets you treat methods like normal attributes—perfect for adding validation or extra logic without changing how your code uses them. Here’s what it looks like using a car as an example:
class Car:
def __init__(self, make, model, mileage=0):
self.make = make
self.model = model
self._mileage = mileage # Notice the underscore for “internal use”
@property
def mileage(self):
# Called when you do car.mileage
return self._mileage
@mileage.setter
def mileage(self, value):
# Called when you do car.mileage = value
if value < 0:
raise ValueError("Mileage cannot be negative!")
self._mileage = value
# Example usage:
dream_car = Car("Porsche", "911", 100)
print(dream_car.mileage) # Accesses via @property → prints 100
dream_car.mileage = 150 # Uses @mileage.setter → sets _mileage to 150
dream_car.mileage = -10 # Raises ValueError
Breaking It Down
self._mileage
is just a normal instance attribute we’re treating as “private” by convention (leading underscore). By doing this, you can keep an eye on how it’s modified, so you don’t accidentally mess up your car’s data behind the scenes.@property
in front ofmileage()
means, “Whenever someone doesdream_car.mileage
, we’ll quietly run this method to fetch and return_mileage
.” To the outside world, it looks just like a typical attribute.@mileage.setter
says, “When you dodream_car.mileage = some_value
, let’s first check if everything’s cool (e.g., no negative mileage) before actually updating_mileage
.” This is super handy for validating data or doing other behind-the-scenes tasks while still keeping your code clean and readable.
The end result is that dream_car.mileage
acts like a regular variable, but under the hood, there’s a bit of magic going on to keep everything running smoothly. That’s the real beauty of properties!
Common Use Cases (and Pitfalls)
Validation
Like preventing a negative radius in a circle, you might want to ensure a car’s mileage is never negative. With properties, you can throw an error if someone tries to assign an invalid value.Calculation on the Fly
Maybe you store the car’s total mileage, but sometimes you want to calculate how many miles you can drive until your next maintenance. A property could compute or transform that data as needed without requiring a separate attribute.Changing Underlying Data Representation
If you once stored mileage in kilometers but now prefer miles (or vice versa), you can keep the property name the same—mileage
—and just do the conversion behind the scenes. This way, the rest of your code doesn’t need to change.
Watch Out For…
Infinite recursion: If you try to do something like
self.mileage = value
from inside your@mileage.setter
, you’ll end up calling the setter repeatedly. Always update the actual attribute (e.g.,self._mileage
) to avoid that loop.Name collisions: Typically, you’ll name the property method the same as its underlying attribute (
mileage
). To prevent confusion, store the real data in a separate variable with an underscore, likeself._mileage
.
A Quick Comparison
Feature | Attributes | Properties |
Definition | Basic data, stored as instance or class variables | Enhanced attribute with built-in getters/setters |
Access Style | my_car.make | my_car.mileage (actually calls a method behind the scenes) |
Main Benefit | Straightforward, easy to set and get data | Control, validation, computed values, and more |
Typical Syntax | Just self.make = make in __init__ | @property and @prop.setter decorators |
Example in Action
Let’s take a look at a simple example. We want to make sure the car’s mileage is never set to a negative number—because that would be, well, a bit odd:
class Car:
def __init__(self, make, model, mileage=0):
self.make = make
self.model = model
self._mileage = mileage # Using an underscore to indicate "internal" usage
@property
def mileage(self):
return self._mileage
@mileage.setter
def mileage(self, value):
if value < 0:
raise ValueError("Mileage cannot be negative!")
self._mileage = value
# Usage
my_car = Car("Porsche", "911", 100)
print(my_car.make) # "Porsche"
print(my_car.model) # "911"
print(my_car.mileage) # 100
my_car.mileage = 150 # Valid update
print(my_car.mileage) # 150
my_car.mileage = -10 # Raises ValueError
Here, we’re “secretly” checking the incoming mileage to ensure it isn’t negative before we store it in _mileage
. To the rest of our code, my_car.mileage
looks and behaves just like a plain old attribute—but we’re adding a layer of validation behind the scenes.
Conclusion
Attributes are regular variables on your instance or class—no special behavior when you set or get them.
Properties are enhanced attributes that look normal, but actually call methods behind the scenes, letting you do validation, run calculations, or log activity each time they’re accessed or modified.
Properties are a great way to keep your code clean. You get the ease of object.attribute
syntax while still having the power of method calls. That’s why you’ll often see them in Python code: they allow you to encapsulate functionality without sacrificing readability.
Now that you’ve seen how attributes vs. properties can work in a simple car example, I hope they feel a bit less mysterious. And if it’s still fuzzy, remember: I’m sharing this as I understand it. Keep tinkering with them, experiment with getters and setters, and before you know it, it’ll all click. Happy coding!