Skip to content

Building Basic Logic in Python

Lesson 5: Conditionals

In our work, we often want to our programs to be make decisions based on what's going on at the time. Like most other programming languages, python supports conditional statements. The way in which we make decisions in our code based on conditions is through the if statement.


The boolean, or bool, datatype in Python is useful for logic expressions, and can either be True or False.

print(3 == 2)

mybool = 3 > 2
print("My boolean type  = ", type(mybool))
print("My boolean value = ", mybool)
My boolean type  =  <class 'bool'>
My boolean value =  True

The if Statement

Let's start with an illustrative example of the state of matter of water (H2O) based on the temperature:

water_temperature = -1 # degrees celsius

Below is the simple if statement: "if the temperature of the water is greater than 100 °C, it must be boiling"

if water_temperature > 100:

Now, let's add an else catch to our if statement: "if ... greater than ..., __otherwise__, the water is not boiling"

if water_temperature > 100:
    print('Not boiling!')
Not boiling!

We can increase the number of levels of if statement in the following manner:

if (condition #1): execute code for case #1 ... . . . elif (condition #k): execute code for case #k ... . . . elif (condition #n): execute code for case #n ... else: execute "catch all" case ...

In principle, there is no limit to the number of intermediate elif or "else, if" checks.

water_temperature = -5
if water_temperature > 100:
elif water_temperature > 0:

Note that conditions are demarcated using indentation. This is a contentious property of Python, but is implemented primarily because it makes code much easier to read. Note also that the indentation level can be almost any combination of whitespace characters (tabs and spaces), but elements of the same indentation block must use the same type of indentation.

Because it gets confusing to use multiple types, the python style guide strongly recommends using 4 spaces to indent, which is the default in the jupyter notebook for example. This is sometimes more difficult to toggle in text editors, but most of them have online resources for setting up your text editor to use tabs and indentation in a way that conforms to python's style.

What is truth?

We can typecast another datatype as bool using bool() (we can do this for other fundamental datatypes such as int and float).

The results of some of these casting examples are quite interesting!


Be very careful with this! Python's "falsy" functionality makes code fun to write and easier to read, but can cause problems. For example, many people will put a simple if statement as a conditional on whether the variable is stored as a falsy value (e. g. None).

Part of the reason we're mentioning this today is that this is often the case in codes that use a lot of serialization like pymatgen.

temperature = 0 # store the temperature as a number if you want it to do something
if temperature:
    print("Temperature is", temperature)

Lesson 6: Sets and Dictionaries

Use a set to store unique values

  • Possible to initialize a set with {...}
  • But must use set() to create an empty set
primes = {2, 3, 5, 7}
print('is 3 prime?', 3 in primes)
print('is 9 prime?', 9 in primes)
is 3 prime? True
is 9 prime? False

* Intersection, union, etc.

odds = {3, 5, 7, 9}
print('intersection', odds & primes)
print('union', odds | primes)
intersection {3, 5, 7}
union {2, 3, 5, 7, 9}

Sets are mutable

  • But only store unique values
print('primes becomes', primes)
print('after removal', primes)
print('after adding 11 again', primes)
primes becomes {2, 3, 5, 7, 11}
after removal {2, 3, 5, 11}
after adding 11 again {2, 3, 5, 11}

Sets are unordered

  • Values are stored by hashing, which is intentionally as random as possible
names = {'Hopper', 'Cori', 'Kohn'}
for n in names:

Use a dictionary to store key/value pairs

Equivalently, store extra information with elements of a set.

birthdays = {'Hopper': 1906, 'Cori': 1896}
birthdays['Kohn'] = 1823 # oops
birthdays['Kohn'] = 1923 # that's better
{'Hopper': 1906, 'Cori': 1896, 'Kohn': 1923}

* Just an accident that keys are in order of when entered. * Like sets, dictionaries store keys by hashing, which is as random as possible

Set values and dictionary keys must be immutable

  • Changing them after insertion would leave data in the wrong place
  • Use a tuple for multi-valued keys
people = {('Grace', 'Hopper'): 1906, ('Gerty', 'Cory'): 1896, ('Walter', 'Kohn'): 1923}

You can destructure a tuple in the heading of a for loop:

for (first, last) in people:
    print(first,'was born in', people[(first, last)])
Grace was born in 1906
Gerty was born in 1896
Walter was born in 1923

Lesson 7: Writing Functions

Break down programs into functions

  • Readability: human beings can only keep a few items in working memory at a time. Encapsulate complexity so that we can treat it as a single “thing”.
  • Reuse: write one time, use many times.
  • Testing: components with well-defined boundaries are easier to test.

Define a function using def with a name, parameters, and a block of code

  • Function name must obey the same rules as variable names
  • Put parameters in parentheses
  • Then a colon, then an indented code block
# Empty parentheses if the function doesn't take any inputs:
def print_greeting():

Arguments in call are matched to parameters in definition

def print_date(year, month, day):
    joined = str(year) + '/' + str(month) + '/' + str(day)

print_date(1871, 3, 19)

Functions may return a result to their caller using return

  • May occur anywhere in the function
  • But functions are easier to understand if return occurs
  • At the start, to handle special cases
  • At the very end, with a final result

  • Functions without explicit return produce None

def average(values):
    if len(values) == 0:
        return None
    return sum(values) / len(values)
a = average([1, 3, 4])
print('average of actual values:', a)
average of actual values: 2.6666666666666665

print('average of empty list:', average([]))
average of empty list: None

result = print_date(1871, 3, 19)
print('result of call is:', result)
result of call is: None

Can specify default values for parameters

  • All paramters with defaults must come after all parameters without.
  • Otherwise, argument-to-parameter matching would be ambigious.
  • Makes common cases simpler, and signals intent
def my_sum(values, scale=1.0):
    result = 0.0
    for v in values:
        result += v * scale
    return result

print('my_sum with default:', my_sum([1, 2, 3]))
print('sum with factor:', my_sum([1, 2, 3], 0.5))
my_sum with default: 6.0
sum with factor: 3.0

# Succinctly...
def my_sum(values, scale=1.0):
    return sum(v * scale for v in values)

Can pass parameters by name

  • Helpful when functions have lots of options

    If you have a procedure with ten parameters, you probably missed some.
    -- from "Epigrams in Programming", by Alan J. Perlis

print('out of order:', my_sum(scale=0.25, values=[1, 2, 3]))
out of order: 1.5

Functions can take a variable number of arguments

  • Prefix at most one parameter's name with *.
  • By convention, everyone calls the parameters *args.
  • All "extra" paramters are put in a list-like structure assigned to that parameter
def total(scale, *args):
    return sum(a * scale for a in args)

print('with one value:', total(0.5, 1))
print('with two values:', total(0.5, 1, 3))
with one value: 0.5
with two values: 2.0

Functions can return multiple values

  • This is just a special case of many-to-many assignment
red, green, blue = 10, 50, 180

def order(a, b):
    if a < b:
        return a, b
        return b, a

low, high = order(10, 5)
print('order(10, 5):', low, high)
order(10, 5): 5 10