3  Syntax Basics

3.1 Introduction

We’ll take a look at some of the fundamental aspects of the Python syntax. Our aim is not to be exhaustive and replicate the whole python official documentation, but to get down the basics that will allow us to start writing simple programs and building from there as we go.
Also: It is fine not to remember everything after the first read, we rather want to get familiar with the language and some of the common idioms – just go with the flow :)

3.2 Basic types

We have numbers:

1 + 2
3
type(1), type(2)
(int, int)
type(1 + 2)
int

As we see, adding two integers results in an integer. We can also represent non-integer numbers as floats:

1.2 + 1.3
2.5
type(1.2 + 2.3)
float

Also notice:

type(1)
int
type(1.0)
float
type(1.)
float

Also, these three numbers evaluate to equal, as we can check using the equality operator:

1 == 1. == 1.0
True

If we add an int to a float, we get:

type(1 + 1.)
float

Floating point numbers are an approximation:

0.1 + 0.2
0.30000000000000004
Tip

For now: A floating point is just a way that computers have to approximately represent numbers. For the most part, we will not have to think too much about them starting our Python journey.

In case you want to learn more about floating point numbers you can watch this great video:

Python also has strings:

type("Hi there")
str
"Hi there" == 'Hi there'  # Notice both " and ' can be used
True

Strings are very powerful and have many “methods” associated with them that facilitates manipulating them. For example:

Note

In this context, a “method” is just a function attached to another entity, for example a string. We’ll learn more about them later on.

"HELLO World".lower()
'hello world'
"HELLO World".startswith("he")
False
"HELLO World".lower().startswith("he")
True
"HELLO World".endswith("d")
True

There are more methods defined on strings.
Whenever you’re trying to do some operation over a string consider checking first if the method is already there. If you are in an interactive environment like the REPL or a jupyter notebook, you can type “.” (dot) and hit TAB for suggestions, for example:

We can format strings using a handy language construct: f-strings, which allow us to easily manipulate strings in a dynamical fashion, even executing code inside them. For example:

f"Hello world, I think {1 + 1} = 2"
'Hello world, I think 2 = 2'
f"The {'INNER SHOUTING STRING'.lower()} should be lower"
'The inner shouting string should be lower'

Notice that we alternated ” and ’.

We’ll see more examples of f-strings later on, as they are a super handy tool adding a lot of expresivity to the language.

3.3 Variables

We can store values and use them later with variables:

first_name = "Nik"
last_name = "Mamba"
age = 23

For example, to format a string:

f"name is {first_name}, the last name {last_name}. Their age={age}"
'name is Nik, the last name Mamba. Their age=23'

Or with this handy f-string substitution:

f"{first_name=}, {last_name=}. Their {age=}"
"first_name='Nik', last_name='Mamba'. Their age=23"
Warning

We need to be careful with the behaviour of variables depending on their type. We will get back to that in a few paragraphs after we talk about mutability.

3.4 Lists

We can store elements in different kinds of containers, collections of elements.

list is the most common of them. We can store basically anything in them, including repetitions of the same element:

short = ["having fun", 123]
type(short)
list
mixed = ["hello", 1, 2., short, short]
mixed
['hello', 1, 2.0, ['having fun', 123], ['having fun', 123]]

We can access the elements of the list via their indices (starting at 0)

mixed[0], mixed[-1]  # Notice -1: refers to the last element
('hello', ['having fun', 123])

We can take parts of that list, a so-called slice, by indicating a slice of indexes. The slice has the syntax (start, stop, [step]), the step is optional and can be negative. The slice is inclusive on the left and exclusive on the right:

numbers = [1, 2, 3, 4, 5, 6]
numbers[1: -1]  # Start from second until last (not including it)
[2, 3, 4, 5]

Importantly, we can modify a list:

mixed
['hello', 1, 2.0, ['having fun', 123], ['having fun', 123]]
mixed[0] = 100
mixed
[100, 1, 2.0, ['having fun', 123], ['having fun', 123]]

You should think of lists as a sequence of references to other elements (objects). Here’s a practical example:

short = ["python", 1, 2]
long = [1, 2, 3, 4, 5, short, short]
short[1] = "is"
short[2] = "fun"
short
['python', 'is', 'fun']
long
[1, 2, 3, 4, 5, ['python', 'is', 'fun'], ['python', 'is', 'fun']]

Since long is simply holding a reference to short, if we modify short, we’ll see that change propagate to long.

We can add elements to a list using the .append method:

result = [1, 2]
result.append("hello again")
result
[1, 2, 'hello again']

Removing elements is also possible:

result.remove?
Signature: result.remove(value, /)
Docstring:
Remove first occurrence of value.
Raises ValueError if the value is not present.
Type:      builtin_function_or_method
result.remove(1)
result
[2, 'hello again']
result.remove(1)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[37], line 1
----> 1 result.remove(1)

ValueError: list.remove(x): x not in list
Note

Mutability
In Python some types can be modified in-place (thus we say there are mutable), for example list. Others can’t be modified after creating them, thus immutable, for example tuple.

Tip

Explore the buil-int methods associated to list, there are handy ones, like .sort

Note

Names and Variables can be a bit tricky in Python. If you are interested in some more details about the inner workings, I recommend this talk

3.5 Tuples

Similar to list but immutable, which gives them some performance advantages. In practice, you should prefer them over lists when care about the mutability (see example below).

sports = ("tennis", "football", "handball")
sports[:2]  # Indexing also works here
('tennis', 'football')
sports[0] = "not gonna work"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[40], line 1
----> 1 sports[0] = "not gonna work"

TypeError: 'tuple' object does not support item assignment

In a subsquent chapter we will cover a more useful variation of tuple, the NamedTuple.

3.6 Sets

Sets will guarantee at most 1 occurrence of each element, so it’s ideal to maintain a container with unique elements.

unique_numbers = {1, 2, 2, 2, 2, 2, 2, 3}
unique_numbers
{1, 2, 3}
unique_numbers.add(4)
unique_numbers
{1, 2, 3, 4}
unique_numbers.add(2)
unique_numbers
{1, 2, 3, 4}
Warning

Sets don’t preserve insertion order!

{"hello", "world", 1,2, "ciao", 4}
{1, 2, 4, 'ciao', 'hello', 'world'}

Sets are also much faster for lookups than lists and tuples. Let’s look at an example:

numbers_list = list(range(100_000_000))
numbers_tuple = tuple(range(100_000_000))
numbers_set = set(range(100_000_000))
%%time
500_000_000 in numbers_list
CPU times: user 739 ms, sys: 158 µs, total: 739 ms
Wall time: 735 ms
False
%%time
500_000_000 in numbers_tuple
CPU times: user 712 ms, sys: 0 ns, total: 712 ms
Wall time: 709 ms
False
%%time
500_000_000 in numbers_set
CPU times: user 3 µs, sys: 0 ns, total: 3 µs
Wall time: 7.63 µs
False

3.7 Dictionaries

A dictionary is a mapping from keys to values. Importantly, the keys of a dictionary are unique. The values can be literally anything.

word2num = {
    "learning": 1,
    "python": 2,
    "is": 3,
    "fun": 4
}
word2num
{'learning': 1, 'python': 2, 'is': 3, 'fun': 4}

We can access them:

word2num["is"]
3
word2vec = {
    "python": [1,2,3],
    "tennis": [4,5,6]
}
word2vec
{'python': [1, 2, 3], 'tennis': [4, 5, 6]}
nested = {
    "first": {
        "one": 1,
        "two": 2,
    },
    "second": {
        "three": 3,
        "four": 4,
    },
}
nested
{'first': {'one': 1, 'two': 2}, 'second': {'three': 3, 'four': 4}}
nested["second"]["four"]
4

It’s important to keep in mind that dictionaries consist of items that are key-value pairs. That means we can build them from those paired items:

items = [
    ("one", 1),
    ("two", 2),
    ("three", 3),
]
items
[('one', 1), ('two', 2), ('three', 3)]
pairs = dict(items)
pairs, type(pairs)
({'one': 1, 'two': 2, 'three': 3}, dict)

We often want to iterate through the elements of a dictionary.

By default iterating over a dictionary goes over the keys:

for key in pairs:
    print(key)
one
two
three
for key in pairs.keys():
    print(key)
one
two
three
for val in pairs.values():
    print(val)
1
2
3
for item in pairs.items():
    print(item)
('one', 1)
('two', 2)
('three', 3)
keys = pairs.keys()
keys
dict_keys(['one', 'two', 'three'])
vals = pairs.values()
vals
dict_values([1, 2, 3])

We can combine two collections, for example two lists, with the zip function. zip interleaves elements and stops as soon as the shortest collection is consumed:

dict(zip(keys, vals))
{'one': 1, 'two': 2, 'three': 3}

The inverse operation to zip can be done by unpacking (with *) the arguments like this:

list(zip(*zip(keys, vals)))
[('one', 'two', 'three'), (1, 2, 3)]

In Python we can “unpack” sequences directly during assignment:

seq = [1,2]
first, second = seq
first, second
(1, 2)
seq = (1, 2, 3, 4)
first, *rest = seq
first, rest
(1, [2, 3, 4])
first, *middle, last = seq
first, middle, last
(1, [2, 3], 4)

We can iterate and unpack the items of a dictionary on the fly:

for key, val in pairs.items():
    print(f"{key} -> {val}")
one -> 1
two -> 2
three -> 3

We can modify a dictionary, by updating an item or items:

pairs
{'one': 1, 'two': 2, 'three': 3}
pairs.update({"one": 111})
pairs
{'one': 111, 'two': 2, 'three': 3}
pairs["four"] = 4
pairs
{'one': 111, 'two': 2, 'three': 3, 'four': 4}

Dictionary keys must be immutable. That’s a perfect use case for tuples too:

mutable_key = [1,2,3]
immutable_key = (1,2,3)
{mutable_key: "not gonna work"}  # Try to build a dictionary
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[8], line 1
----> 1 {mutable_key: "not gonna work"}  # Try to build a dictionary

TypeError: unhashable type: 'list'
d = {immutable_key: "that looks better"}

And we can retrieve that value as per usual:

d[(1,2,3)]
'that looks better'
d[1,2,3]  # optionally without parenthesis
'that looks better'

All these built-in types that we’ve seen so far can take you already pretty far in manipulating data, both for quick exploration and for more detailed analysis. So it will really pay off to have a good grasp on them, take your time to learn them!

Here’s a talk for you to watch later that hammers home this point with great mastery:

3.8 Exercises

  1. Create variables for the name of your favorite book and its author. Use an f-string to print a sentence that says, “My favorite book is [book] by [author].”
  2. Repeat it, but the book and author should be CAPITAL.
  3. Repeat it, but the author and book should be each one a single word connected by hyphens, eg: “Charles Darwin” -> “charles-darwin”.
  4. Create a list of your 3 favorite books. Print the length of the list.
  5. Create a list of your 3 favorite sports.
  6. Concatenate the two previous lists and print the result.
  7. Print the last 2 elements of the concatenated list.
  8. Print the third element of the list with its characters in reversed order.
  9. Create a tuple containing 5 different cities. Print the first and last city from the tuple.
  10. Create a tuple of numbers and use slicing to print the middle three numbers.
  11. Create a dictionary with your favorite fruits as keys and their colors as values. Print the color of a specific fruit.
  12. Create a dictionary with countries as keys and their capitals as values. Print the keys and values separately.
  13. Invert the items of the dictionary, making the keys the values and vice versa.
  14. Create a set of your 3 favorite animals. Check if a specific animal is in the set.
  15. Create two sets of your favorite sports and find the difference between them.