Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Python Basics Exercises: Building Systems With Classes (Summary)

Congratulations! You’ve now practiced using classes for object-oriented programming (OOP) in Python. Specifically, you can:

  • Compose classes together
  • Inherit and override behavior from other classes
  • Creatively mix and match these approaches

With these skills to your name, you’re now ready to tackle creating complex systems and writing sleek, Pythonic code.

If you want to dive deeper, then these written tutorials are for you:

Or you can explore the following video courses:

OOP is a big topic, and Real Python has more resources to help you expand your skill set. There’s even a learning path that’ll help you solidly grasp the fundamentals of OOP so that you can make your programs easier to write and maintain.

To continue your Python learning journey, check out the other Python Basics courses. You might also consider getting yourself a copy of Python Basics: A Practical Introduction to Python 3.

Download

Sample Code (.zip)

17.7 MB
Download

Course Slides (.pdf)

17.4 MB

00:00 Congratulations on making it through this Python Basics Exercises course. Here’s a quick summary of what you did, and I just want to highlight that there was a lot that you did in this course.

00:10 You practiced using classes, class attributes, instance attributes, non-public attributes, instance methods, even properties. You used special methods, you did inheritance a couple of times, you used composition and aggregation, used f-strings, and of course, you hopefully also practiced to use pen and paper and draw out your ideas.

00:34 Additionally to practicing coding concepts, there were also a couple of tips that you came across that might be helpful for your development in general. Draft your idea on pen and paper, use code comments to help you get organized, break exercises into smaller tasks, revisit your plan if you want to and update it.

00:51 Don’t worry about throwing out ideas that you had at the beginning while developing your code. Things might have changed. It’s always helpful to use descriptive variable names. That just makes reading your code so much easier.

01:03 It’s also helpful to create readable logging output is what I call this. Here in this course, I mostly did it by creating readable .__str__ methods and returning f-strings so that when running the code and testing it, the output would give me an idea to understand whether the code does what I expect it to do. Right, and I gave you some tips on how you could move that over to logging instead of using it as your return values. And I already mentioned that a bit, but going to test repeatedly to figure out whether the code actually does what you expect it to do is also very helpful because often you have an idea of what you think, you just code it up, but then it needs to hold up against the test of actually running it.

01:46 Keep practicing while you code. Keep them in mind that these are useful techniques while coding in general, and you can benefit from learning these across all the development that you do.

01:57 Now, here’s the promised additional resources. We have quite a lot in the course catalog about object-oriented programming. In the PDF that you can download that has all these slides, you can click the links on these additional resources, and they’re going to take you straight to the tutorial or video course.

02:14 And we have resources on inheritance and composition, about Python super(), about data classes, which is another way to create objects that is a little less involved than writing the classes the way that you did in this course.

02:28 We have resources on the @property decorator and also on providing multiple constructors to your Python classes.

02:37 There’s also a series of videos about object-oriented programming that tackles different aspects of it, so make sure to check this out as well. And they’re kind of loosely based on the Python classes tutorial that dives into the power of object-oriented programming.

02:54 So make sure to check out these additional resources if you want to learn more about OOP.

02:59 Thanks for joining me in this Python Basics Exercises course, and congratulations for making it all the way through, and hope to see you around at Real Python.

alphafox28js on Dec. 1, 2023

Output of the 98% of Code that Works is below for 1 animal. Functional and was Fun.

All of the Pitbulls are living on the farm. The Pitbulls move with a bold pace. They eat fast. They speak forcefully. Most run with purpose, but some slow. The hyper Pitbulls of the Canine Family jumps over the lazy dogs, and into the None. At night the Pitbulls sleeps peacefully and wake up slowly. All of the Animals and Farmers couldn’t be more happy!

The farm has Rolling Hills, 25mph winds, and large scatterd trees. It sits in the open plains. The Animalia run around and play all day. When it is time to feed, the Animalia, run to the the red barn. They eat from the Field. and to the barn. The Field has 1/10 spaces filled.

Martin Breuss RP Team on Dec. 6, 2023

@alphafox28js haha, I love your output! It’s very poetic!! :D

Ash D on Dec. 24, 2023

So I watched this exercises course after I’d already gone and written my farm modeling code, and it turns out that my code was somewhat similar to Martin’s. I’d even drawn out my design with pen and paper, which I find really useful. The main difference with my code is that as an early design decision I opted to store the animal’s location only within the Habitat object (barn, field, etc) rather than duplicating this relationship in the animal too. At the time, I think I had an irrational aversion to storing the relationship in two places, in case it gets out of sync. I think this aversion comes from some other area (or era) of IT that I’ve worked in, where this was problematic or considered a bad idea. Overall I can see that it’s useful (even vital) to have this relationship duplicated in the animal. There’s ways to iterate through all the locations looking for an animal, but it wouldn’t be elegant.

Also I didn’t do any eating or pooping methods. And it’s possible I have the same logic bug in my move() logic.

farm.py

class Animal:
    species = "Animal"
    default_greeting = "Hello!"

    def __init__(self, name, colour):
        self.name = name
        self.colour = colour

    def __str__(self):
        return f"{self.name} is a {self.colour} {self.__class__.species.lower()}."

    def speak(self, sound=None):
        print(f"{self.name} says '{sound if sound else self.__class__.default_greeting}'")

    def move(self, source, destination):
        # Move animal from one Habitat to another by deleting from source Habitat and adding to dest Habitat
        # Could upgrade this so that the source doesn't need to be specified, but hey this is just a simple exercise
        if self in source.occupants and destination.remaining_spaces > 0:
            source.remove_animal(self)
            destination.add_animal(self)
        else:
            print(
                f"Error: Can't move {self.name} from the {source.name} to the {destination.name}: animal not present "
                f"in the {source.name}")


class Cow(Animal):
    species = "cow"
    default_greeting = "Moo!"

    def __init__(self, name, colour="black and white"):
        super().__init__(name, colour)


class Pig(Animal):
    species = "pig"
    default_greeting = "Oink oink!"

    def __init__(self, name, colour="pink"):
        super().__init__(name, colour)


class Horse(Animal):
    species = "horse"
    default_greeting = "Good day to you, sir."

    def __init__(self, name, colour="black"):
        super().__init__(name, colour)


class Habitat:
    def __init__(self, name, max_occupants):
        self.occupants = []
        self._max_occupants = max_occupants

        # I could've just set default names in the child classes, but wanted to play with dunder attributes
        self.name = name if name is not None else self.__class__.__name__.lower()

    @property
    def inventory(self) -> str:
        rem_space_suffix = f"Remaining spaces: {self.remaining_spaces}/{self._max_occupants}"
        if len(self.occupants) == 0:
            return f"{self.name} inventory: Empty. " + rem_space_suffix
        else:
            animal_names = [f"\t\t* {animal.name} the {animal.species}" for animal in self.occupants]
            return (f"{self.name} inventory: {rem_space_suffix}. There's {len(animal_names)} animals in there: \n" +
                    '\n'.join(animal_names))

    @property
    def remaining_spaces(self) -> int:
        return self._max_occupants - len(self.occupants)

    def remove_animal(self, animal):
        if animal in self.occupants:
            self.occupants.remove(animal)
            print(f"Moved {animal.name} the {animal.species} out of the {self.name.lower()}")
        else:
            print(f"Can't remove {animal.name} the {animal.species} because it isn't in the {self.name.lower()}!")

    def add_animal(self, animal):
        # Don't allow exceeding capacity, or trying to add the same animal twice
        if len(self.occupants) < self._max_occupants and animal not in self.occupants:
            self.occupants.append(animal)
            print(f"Moved {animal.name} the {animal.species} to the {self.name.lower()}.")
        else:
            print(f"{animal.name} the {animal.species} can't fit in the {self.name.lower()} because it's full!")

    def move_all(self, destination):
        # Worth caching this since we're using the value several times below
        num_source_occupants = len(self.occupants)

        # Check if source has any animals to move, and if we have enough space in the destination
        if num_source_occupants == 0:
            print(f"Error: move_all operation called on an empty source habitat - the {self.name} is empty.")
        elif destination.remaining_spaces < len(self.occupants):
            print(f"Can't move all {num_source_occupants} animals from the {self.name} to the {destination.name}, "
                  f"because it only has {destination.remaining_spaces} spaces left!")
        else:
            print(f"Moving {num_source_occupants} animals from the {self.name} to the {destination.name}")
            destination.occupants += self.occupants
            self.occupants.clear()


class Barn(Habitat):
    def __init__(self, name="Barn", max_occupants=3):
        super().__init__(name, max_occupants)


class Field(Habitat):
    def __init__(self, name="Field", max_occupants=10):
        super().__init__(name, max_occupants)

main.py (testing code)

from farm import *

barn = Barn(name="Big red barn")
field = Field()

lucy = Cow(name="Lucy", colour="brown")
rob = Pig(name="Rob")
bonnie = Horse(name="Bonnie", colour="white")
rupert = Horse(name="Rupert")

print(lucy)
print(rob)
print(bonnie)
print(rupert)
print()

lucy.speak(sound="MoooooOOOOOOoooo!")
rob.speak()
bonnie.speak()
rupert.speak()
print()

barn.add_animal(lucy)
barn.add_animal(rupert)
field.add_animal(rob)
field.add_animal(bonnie)
print()

print(barn.inventory)
print(field.inventory)
print()

lucy.move(barn, field)
rob.move(barn, field)
print(barn.inventory)
print(field.inventory)
print()

field.move_all(destination=barn)
print(barn.inventory)
print(field.inventory)
print()

barn.move_all(destination=field)
print(barn.inventory)
print(field.inventory)

Console output: (added some manual line breaks for readability)

Lucy is a brown cow.
Rob is a pink pig.
Bonnie is a white horse.
Rupert is a black horse.

Lucy says 'MoooooOOOOOOoooo!'
Rob says 'Oink oink!'
Bonnie says 'Good day to you, sir.'
Rupert says 'Good day to you, sir.'

Moved Lucy the cow to the big red barn.
Moved Rupert the horse to the big red barn.
Moved Rob the pig to the field.
Moved Bonnie the horse to the field.

Big red barn inventory: Remaining spaces: 1/3. There's 2 animals in there: 
        * Lucy the cow
        * Rupert the horse

Field inventory: Remaining spaces: 8/10. There's 2 animals in there: 
        * Rob the pig
        * Bonnie the horse

Moved Lucy the cow out of the big red barn
Moved Lucy the cow to the field.

Error: Can't move Rob from the Big red barn to the Field: animal not present in the Big red barn

Big red barn inventory: Remaining spaces: 2/3. There's 1 animals in there: 
        * Rupert the horse

Field inventory: Remaining spaces: 7/10. There's 3 animals in there: 
        * Rob the pig
        * Bonnie the horse
        * Lucy the cow

Can't move all 3 animals from the Field to the Big red barn, because it only has 2 spaces left!

Big red barn inventory: Remaining spaces: 2/3. There's 1 animals in there: 
        * Rupert the horse

Field inventory: Remaining spaces: 7/10. There's 3 animals in there: 
        * Rob the pig
        * Bonnie the horse
        * Lucy the cow

Moving 1 animals from the Big red barn to the Field

Big red barn inventory: Empty. Remaining spaces: 3/3

Field inventory: Remaining spaces: 6/10. There's 4 animals in there: 
        * Rob the pig
        * Bonnie the horse
        * Lucy the cow
        * Rupert the horse

Ash D on Dec. 27, 2023

Ah I think the markdown editor made a hash of the console output. It should look like this:

Field inventory: Remaining spaces: 6/10. Theres 4 animals in there: 
     * Rob the pig 
     * Bonnie the horse 
     * Lucy the cow 
     * Rupert the horse

Become a Member to join the conversation.