edit

Remarks

Significant whitespace

Python is a language where space matters ... meaning units of code (blocks, function bodies, etc.) are delimited by a colon (:) and indentation (4 spaces by convention) of all the following lines that belong to that block. A good editor that is language aware will help with that. It indents the code automatically after ending a line with a colon. It also lets you indent and dedent entire blocks of code that are marked by pressing the Tab key and outdents them when pressing Shift + Tab.

See also: code layout in PEP8.

Example:

def my_super_function():
    print("I am indented with 4 spaces.")
    print("Me too! We both belong to the function!")
print("I am not inside the function block anymore :(")

for currentElement in range(5):
    print(currentElement)
    print("I also belong to the loop block")
print("I don't belong to the loop block anymore")

Everything is an object

Everything. Even functions, classes, modules and files. Everything.

In this Python Online Tutor example you can see how really, really everything in a running Python program is an object.

Passing by assignment

Remember that arguments are passed by assignment in Python. Since assignment just creates references to objects, there’s no alias between an argument name in the caller and callee, and so no call-by-reference per se.

-- How do I write a function with output parameters (call by reference)?

The way passing data to functions work in Python is quite specific, so it is important that you are aware of it and understand it. Walk through this example in the tutor to visualise what is really happening when you pass mutable objects into functions and e.g. append elements to a list object that was passed into a function. In the example it is a list but this holds true for any object that contains references to other objects.

Changing the state of an object that is not returned explicitly is called a side effect. Purists of certain programming paradigms would tell you that this style is messy and error prone. I won't argue with them, because I might lose. For now that's how we do it here, because than you really understand how it works. Real world programs have lots of side effects anyway so better just get used to it :) The discussion around when and how to use side effects is a huge topic. For now I just want to make you aware that some of our functions and methods have side effects, meaning that not all changes to the state of the program are communicated purely by returning values. BTW: raising exceptions are also considered side effects and they are used a lot in Python.

Magic methods (protocols)

Those __something__() thingies might look scary for the uninitiated, but you will love them, once you have grasped the idea. These methods are a way to use the internal language mechanics of Python for your own classes. They make up an important part of the Python superpowers and it's never too early to learn about them (you should at least know that they exist and that they have special meaning). Some of them are used in the model classes to create pythonic behaviour of the objects (e.g. make them iterable and comparable) and good representations.

Object representation (__repr__)

If at all possible, this should look like a valid Python expression that could be used to recreate an object with the same value

-- Python docs

In this simple simulation this is actually possible for all objects, so why not do it? This makes it possible to copy object representations from the output and recreate them in the REPL to experiment with them. If done correctly, this also works when using inheritance (see Stock and Waste).

This could also be useful: reprlib helps making better representations.

Make your own objects behave like built in data types

Other uses of special object attributes

All objects have a name (__name__)

The name attribute of module objects are set dynamically depending on the context in which the module is loaded. If the module is run like a script it has a different name than when it is imported by another module. The names of modules are used for two purposes in this program:

  1. Set the name of the logger object to get information from where the log was written
  2. If a module is started directly it has the special name __main__ - this can be used to only execute certain code if it is meant to behave like a script (as opposed to being imported as a module). This is the canonic way to do this.

More resources about magic methods

Assertions

What can be asserted without evidence can be dismissed without evidence.

-- Christopher Hitchens

To assert something means "to state or express positively". Assertions are regarded as important enough in Python that assert is a statement (since Python 3 even print is not important enough to be a statement). assert evaluates an expression and raises an AssertionError if the result of the evaluation is False (with a customizable message to provide more information about the problem). This can be a very simple check like making sure that an object is truthy if evaluated as bool.

def spam(someObject):
    assert someObject, f"Hey! {someObject} is not what I want!"
    print(someObject)

spam([1, 2])
spam([])

The assert in the spam function makes sure that the argument passed evaluates to True before moving on. The first call is o.k. but the second raises the exception and prints the message as part of the traceback.

This is a good way to make sure that your program crashes early if the preconditions are not what you expect them. It's like making sure there is a chair there before you sit down. Used with good measure this can safe you a lot of trouble - finding the good measure for usage of the assert statement in your code is an art and not a science.

Look for uses of the assert statement in the code to get an idea how it might be used.

Logging

Python has an easy-to-use and convenient logging module included. There is no reason why beginners shouldn't learn to use that right away. This is better than cluttering the code with calls to print. With logging you have the possibility to use different log levels and adjust the output when debugging problems. You can set the level to logging.DEBUG to see the full story or even disable it when running thousands of simulations, where logging would slow the program down.

We use a simple format and the convenience function to initialise the logger to write to the terminal with a certain level.