Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
127: Properties and the @property Decorator
A few years ago, I was reviewing a PR from a junior dev who had spent an entire Friday refactoring a massive project. He had realized that a public attribute—balance in a BankAccount class—needed validation to prevent it from ever going negative. To fix it, he did what he’d learned in his Java classes: he renamed the attribute to _balance and wrote get_balance() and set_balance() methods. The problem? He then had to hunt through fifteen different files and change every single instance of account.balance to account.get_balance(). It was a tedious, error-prone nightmare that broke half the unit tests.
In Python, we avoid this "getter/setter" churn. We believe in starting simple. If a piece of data can be a public attribute, leave it as one. But when the day comes that you suddenly need to add logic—like validation or a calculated value—you use the @property decorator. It allows you to define a method that looks and behaves like an attribute to the outside world. This means you can change the internal implementation without breaking a single line of code in the rest of your application.
Turning Methods into Attributes
Let's say you're building a system to track weather data. You store the temperature in Celsius, but you frequently need it in Fahrenheit. Instead of creating a separate method like get_fahrenheit(), you can use @property.
class WeatherStation:
def __init__(self, celsius):
self._celsius = celsius
@property
def fahrenheit(self):
print("Calculating conversion...")
return (self._celsius * 9/5) + 32
station = WeatherStation(25)
# Notice there are no parentheses here! It looks like a variable.
print(station.fahrenheit)
By adding @property, you've created a "read-only" attribute. If you tried to run station.fahrenheit = 80, Python would throw an AttributeError. This is incredibly useful for values that are derived from other data; you ensure the Fahrenheit value is always in sync with the Celsius value because it's calculated on the fly.
Adding Logic with Setters
Read-only is great, but usually, you want to be able to update the value while still keeping a guardrail in place. This is where the setter comes in. To create a setter, you use the decorator @property_name.setter. This allows you to intercept the assignment operation and run whatever validation logic you need.
I usually suggest using a leading underscore (like _temperature) for the actual variable. This is a Python convention telling other developers, "Hey, this is internal; please don't touch it directly."
class WeatherStation:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero!")
print("Updating temperature...")
self._celsius = value
station = WeatherStation(20)
station.celsius = 25 # This triggers the setter method
# station.celsius = -300 # This would raise the ValueError
The magic here is that the user of your class doesn't need to know that celsius is actually a pair of methods. They just use the equals sign. If you decide later that you need to log every temperature change to a database, you just add one line to the setter method, and the rest of your codebase remains untouched. It's the ultimate "safety valve" for API design.
📋 Practical Task
Building a Smart Home Thermostat Validator
You are designing the software for a smart thermostat. For safety reasons, the thermostat should only allow temperature settings between 50°F and 90°F. Any value outside this range should raise a ValueError. Additionally, the thermostat should always store the temperature as an integer, so if a float is provided, it should be rounded to the nearest whole number.
Requirements:
- Create a class named
Thermostat. - Initialize it with a
_temperatureattribute. - Implement a
@propertyfortemperatureto allow reading the current setting. - Implement a
@temperature.setterthat:- Rounds the incoming value to the nearest integer.
- Checks if the rounded value is between 50 and 90 (inclusive).
- Raises a
ValueErrorwith the message "Temperature out of safe range" if it is not.
Test your code with the following:
t = Thermostat(70)
t.temperature = 72.6 # Should be stored as 73
print(t.temperature) # Should print 73
t.temperature = 110 # Should raise ValueError
There are no comments for now.