Python for Designers

by Roberto Arista

Fork me on GitHub

How to Browse Sequences

We already had a taste of what sequences are thanks to strings. But strings only include text. What if we need to store a bunch of numerical values? Or some strings together with numerical values?

Python provides two standard data types for containers where order matters: a mutable one, list(), and an immutable one, tuple(). They store an ordered sequence of value references. This means that if a list contains another list – of course this is possible – the inner one will not be copied inside the container, but a reference to it will be stored.

In Python a list is delimited by the characters [], while a tuple is delimited by characters ().

Here’s a list:

years = [2018, 2017, 2016, 2015]

Now, this is a tuple:

coordinates = (10.4, 20.2)

As we said, elements in lists and tuples are of arbitrary nature: they can be any kind of object, None included. They can be empty too.

Emptiness usually does not make much sense for tuples, given they cannot be modified once created. Instead it is very common to initiate an empty list which will be populated during an iterative process triggered by a while or for construct. Consider the following example:

from random import random
myRandomNumbers = []    # empty list 
for ii in range(20):
    myRandomNumbers.append(random())

The constructors list() and tuple() accept any kind of iterable as argument like strings or lists or tuples. They “listify” or “tuplefy” their arguments. For example, if a string is passed as argument to a list() function, the result will be a container where each character is stored separately:

from string import ascii_lowercase
myLowercase = list(ascii_lowercase)
# ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']

At this point, you might ask yourself: why both tuples and lists? Aren’t lists enough? Apart from the difference in terms of mutability, there is a semantic difference in the usage of tuples and lists.

While tuples are used to store heterogeneous data structures, lists are ordered sequences of the same stuff. Take note that they have the same degree of freedom concerning data types, it’s just a matter of semantics.

Let’s consider the following scenario: it’s time for a quick ride on our bike and we would like to track our journey using an application installed on a smartphone. Let’s assume this application is written in Python. Every few seconds the application will ask the smartphone hardware for:

  • gps coordinate x
  • gps coordinate y
  • date and time
position = (x, y, dateTime)

This data will be then organized in a tuple. Since the length of the container does not need to be flexible (e.g. we know we won’t add a z index to it) but just to record a state through multiple values, a tuple is a good choice.

The application’s goal is to track a route, which is a sequence of multiple position instances. So, the application needs a container which is flexible and can be extended until the end of the journey: a list.

route = [position01,
         position02,
         position03]

Once finished, the route of the journey is saved on the smartphone memory. A standard table would suit very well the purpose.

Sequence Properties for Lists and Tuples

  • accessing
  • slicing
  • check containment
  • test equality
  • natural order
  • concatenation

List Methods

As we said, a list is a flexible container, which means that once created, it can be manipulated in multiple ways. Python provides a few specific methods for it

.append(item)

# create a list
myList = [1, 'b', 2]
# append an element at the end of the list
myList.append('d')
# result: [1, 'b', 2, 'd']

.extend(iterable)

from string import ascii_lowercase, ascii_uppercase
# convert from strings to lists using the list() constructor
latinLowercase = list(ascii_lowercase)
latinUppercase = list(ascii_uppercase)
# create an empty list for the complete alphabet
latinAlphabet = []
# extend the empty list with uppercase and lowercase
latinAlphabet.extend(latinUppercase)
latinAlphabet.extend(latinLowercase)
# result: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']

.insert(index, item)

someCoordinates = [(11.2, 32.1),
                   (43.9, 14.8)]

firstPoint = (34.1, 76.4)
someCoordinates.insert(0, firstPoint)
# result: [(34.1, 76.4), (11.2, 32.1), (43.9, 14.8)]
lastPoint = (63.1, 87.3)
someCoordinates.insert(len(someCoordinates), lastPoint)
# result: [(34.1, 76.4), (11.2, 32.1), (43.9, 14.8), (63.1, 87.3)]
aPoint = (87.4, 6.2)
someCoordinates.insert(2, aPoint)
# result: [(34.1, 76.4), (11.2, 32.1), (87.4, 6.2), (43.9, 14.8), (63.1, 87.3)]
anotherPoint = (45.9, 98.7)
someCoordinates.insert(200, anotherPoint)
# result: [(34.1, 76.4), (11.2, 32.1), (87.4, 6.2), (43.9, 14.8), (63.1, 87.3), (45.9, 98.7)]
# insert does not complain if the index is way bigger than the list
# it will put the element at the end of the list without raising an IndexError

exercise 12.1

transform a right-angled triangle in a square adding a point

.remove(item)

myRandomNumbers = [8, 6, 5, 5, 0, 1, 7, 7, 6, 7, 2, 5, 8, 6, 4, 7]
myRandomNumbers.remove(5) # remember: item is not the position index!
# result: [8, 6, 5, 0, 1, 7, 7, 6, 7, 2, 5, 8, 6, 4, 7]
myRandomNumbers.remove(5)
# result: [8, 6, 0, 1, 7, 7, 6, 7, 2, 5, 8, 6, 4, 7]
myRandomNumbers.remove(0)
# result: [8, 6, 1, 7, 7, 6, 7, 2, 5, 8, 6, 4, 7]
myRandomNumbers.remove(0)
# result: ValueError: list.remove(x): x not in list
# check if the list contains the element you want to remove
# with in keyword
if 0 in myRandomNumbers:
    myRandomNumbers.remove(0)
# result: [8, 6, 1, 7, 7, 6, 7, 2, 5, 8, 6, 4, 7]

.pop(index)

myRandomNumbers = [8, 6, 5, 5, 0, 1, 7, 7, 6, 7, 2, 5, 8, 6, 4, 7]
aNumber = myRandomNumbers.pop()
# aNumber: 7
# myRandomNumbers: [8, 6, 5, 5, 0, 1, 7, 7, 6, 7, 2, 5, 8, 6, 4]
aNumber = myRandomNumbers.pop(0)
# aNumber: 8
# myRandomNumbers: [6, 5, 5, 0, 1, 7, 7, 6, 7, 2, 5, 8, 6, 4]
aNumber = myRandomNumbers.pop(5)
# aNumber: 7
# myRandomNumbers: [6, 5, 5, 0, 1, 7, 6, 7, 2, 5, 8, 6, 4]
aNumber = myRandomNumbers.pop(24)
# result: IndexError: pop index out of range
# check the len of the list before using pop
# with built-in function len()

.index(item)

from string import ascii_lowercase
myLowercase = list(ascii_lowercase)
myIndex = myLowercase.index('f')
# myIndex: 5

.sort()

percents = [0.362, 0.782, 0.183, 0.045, 0.549]
percents.sort()
# result: [0.045, 0.183, 0.362, 0.549, 0.782]

.reverse()

from string import ascii_lowercase
flippedLowercase = list(ascii_lowercase)
flippedLowercase.reverse()
# result: ['z', 'y', 'x', 'w', 'v', 'u', 't', 's', 'r', 'q', 'p', 'o', 'n', 'm', 'l', 'k', 'j', 'i', 'h', 'g', 'f', 'e', 'd', 'c', 'b', 'a']

For Syntax

Python provides a for-loop syntax which is useful to iterate over a series of elements. Where while comes in handy when we need to keep doing something until a condition is matched, for is preferable when we need to browse a container. for can be used with any kind of iterable, which is a wider notion than sequences. Technically, an iterable can be non-sorted (as sets or dicts).

Here’s the general for construct:

for eachElement in iterable:
    body

Note that the body is four spaces indented rightwards. for and in are protected keywords. eachElement is an identifier to which the body can refer to. Of course its name is arbitrary, it only needs to respect the standard rules for identifiers.

The iterable can be created either before the for opening statement or created on the spot. Meaning that this example:

myList = [1, 2, 3]
for eachNumber in myList:
    print(eachNumber)

is equivalent to:

for eachNumber in [1, 2, 3]:
    print(eachNumber)

This also means that we could invoke a function which is able to generate an iterable on the spot

def makeList():
    return [1, 2, 3]

for eachNumber in makeList():
    print(eachNumber)

Python provides a function that you will use very often in your coding routine: range(). This function returns an iterator –not a real list– which will provide a sequence of integers according to the following arguments

range(start, stop, step)

start and step are optional. start has to be addressed if step is defined. Take into account that, as for the slicing notation, the stop value is not included into the range created.

This function will make the iteration over a sequence of integer number easy and compact:

for eachInt in range(10, 20, 2):
    print(eachInt)

# 10
# 12
# 14
# 16
# 18

After the end of the loop the identifier will be still available in the program namespace, assigned to the last element of the iterable.

for eachInt in range(10, 20, 2):
    print(eachInt)
print('-'*10)
print(eachInt)

# 10
# 12
# 14
# 16
# 18
# ----------
# 18

How can this be useful in a graphic design application? Consider the following scenario: you would like to create a pdf document of sixteen pages and write on each page a sequential page number. DrawBot can do that of course:

pages = 16
for eachPageNumber in range(1, pages+1):
    newPage(200, 200)
    text(f'{eachPageNumber}', (20, 20))

A for construct is the perfect companion of an .append()list method. It is very common to create a series of values starting from a pre-existing one. Like making an all-caps version of a word sequence:

myWords = ['cat', 'table', 'sword']
myAllCapsWords = []
for eachWord in myWords:
    allCapsWord = eachWord.upper()
    myAllCapsWords.append(allCapsWord)
print(myAllCapsWords)
# ['CAT', 'TABLE', 'SWORD']

Drawing Many Times in One Direction

Just like the while construct, for can be very helpful to draw shapes repetitively. In this way your code will look compact and it will be easy to edit.

Instead of:

newPage(100, 100)

stroke(0)
line((20, 20), (80, 20))
line((20, 40), (80, 40))
line((20, 60), (80, 60))
line((20, 80), (80, 80))

go for this kind of structure

linesAmount = 4
newPage(100, 100)

stroke(0)
for eachLineIndex in range(linesAmount):
    # the width() function provides the canvas width in pts
    quota = width()/(linesAmount+1)*(eachLineIndex+1)
    line((20, quota), (80, quota))

The advantages of the second version are enormous, starting for the very basic fact that you can add and subtract elements with almost no changes.


linesAmount = 8
newPage(100, 100)

stroke(0)
for eachLineIndex in range(linesAmount):
    quota = width()/(linesAmount+1)*(eachLineIndex+1)
    line((20, quota), (80, quota))

What if you need to add points at the beginning and at the end? Easily done.

linesAmount = 8
radius = 3
newPage(100, 100)
for eachLineIndex in range(linesAmount):
    quota = width()/(linesAmount+1)*(eachLineIndex+1)
    fill(None)
    stroke(0)
    line((20, quota), (80, quota))

    stroke(None)
    fill(0)
    oval(20-radius, quota-radius, radius*2, radius*2)
    oval(80-radius, quota-radius, radius*2, radius*2)

Along with drawing, we can perform any kind of calculation. For example, the fill color of a shape could be defined by the identifier provided by the for construct. Here’s an example.

shades = 10
newPage(100, 100)

shadeWidth = width()/shades
for shadeIndex in range(shades):
    grayscaleValue = 1 - (1/(shades-1)*shadeIndex)
    fill(grayscaleValue)
    rect(shadeIndex*shadeWidth, 0, shadeWidth, height())

Or we can affect the arguments used to draw the shape itself:

linesAmount = 100
newPage(100, 100)
stroke(0)

factor = 1
for eachLineIndex in range(linesAmount):
    x = width()/(linesAmount+1)*(eachLineIndex+1) * factor
    line((x, 20), (x, 80))
    factor += .1

Remember that if statements can be used in the for body, meaning that we can perform choices within an iterative process:

stripes = 10
newPage(100, 100)

stripeWidth = width()/stripes
for stripeIndex in range(stripes):
    if stripeIndex % 2 == 0:
            fill(0)
    else:
            fill(1)
    rect(stripeIndex*stripeWidth, 0, stripeWidth, height())

Drawing Many Times in Two Directions

A single for construct allows repetition in one dimension. But, as we have seen with the previous example, other constructs can be used inside the for body. Which means that a second for loop can be nested into the first for loop. In this way the repetition becomes two dimensional.

If we need to draw a table or 90° based pattern, this technique becomes very handy. Consider the following drawing

This is a bidimensional matrix, it is a simple table. Each element of the matrix is called cell. Each cell of this matrix can be described by two indexes: the index of the column and the row to which the cell belong. If we explicit write each index to each cell we obtain

At this point we only need to define a size for the cell and we are able to draw the left column of the matrix

rows = 4
newPage(200, 200)

cellSize = height()/rows

stroke(0)
fill(None)
for rowIndex in range(rows):
    rect(0*cellSize, rowIndex*cellSize, cellSize, cellSize)

or the bottom row the matrix

cols = 4
newPage(200, 200)

cellSize = height()/cols

stroke(0)
fill(None)
for colIndex in range(cols):
    rect(colIndex*cellSize, 0*cellSize, cellSize, cellSize)

If we combine the two constructs together we can draw the entire matrix calling the rect() function only once

cols = 4
rows = 4
newPage(200, 200)

cellWidth = width()/cols
cellHeight = height()/rows

stroke(0)
fill(None)
for rowIndex in range(rows):
    for colIndex in range(cols):
        rect(colIndex*cellWidth, rowIndex*cellHeight, cellWidth, cellHeight)

Matrix indexes can be drawn very easily

cols = 4
rows = 4
newPage(200, 200)

cellWidth = width()/cols
cellHeight = height()/rows

for rowIndex in range(rows):
    for colIndex in range(cols):
        x = colIndex*cellWidth
        y = rowIndex*cellHeight

        stroke(None)
        fill(0)
        text(f'{colIndex}, {rowIndex}', (x+5, y+5))

        fill(None)
        stroke(0)
        rect(x, y, cellWidth, cellHeight)

As for the single for construct, calculations performed within the body can affect the quality of the drawing. A one-dimensional gradient can easily become bi-dimensional

cols = 4
rows = 4
newPage(200, 200)
cellWidth = width()/cols
cellHeight = height()/rows
for rowIndex in range(rows):
    for colIndex in range(cols):
        grayscale = (rowIndex+colIndex)/(rows+cols-2)
        fill(grayscale)
        rect(colIndex*cellWidth, rowIndex*cellHeight, cellWidth, cellHeight)

and an if statement nested into the inner for loop can draw a chess board:

cols = 4
rows = 4
newPage(200, 200)
cellWidth = width()/cols
cellHeight = height()/rows
for rowIndex in range(rows):
    for colIndex in range(cols):
        if (rowIndex+colIndex) % 2 == 0:
            fill(0)
        else:
            fill(1)
        rect(colIndex*cellWidth, rowIndex*cellHeight, cellWidth, cellHeight)

Please, note that nested for constructs can also be used to navigate nested data structures as a list of tuples:

myRoute = [(10.2, 22.3, (7, 51, 43)),
           (12.6, 19.8, (7, 52, 2)),
           (14.5, 18.2, (7, 52, 54))
           ]

leading = 18
cellWidth = 50

newPage(200, 200)
font('InputMono-Regular')
fontSize(14)

for indexRow, eachRow in enumerate(myRoute):
    for indexCell, cellContent in enumerate(eachRow):
        x = indexCell*cellWidth
        y = height() - (indexRow+1)*leading

        if indexCell == 2:
            hour, minute, second = cellContent
            text(f'{hour:0>2}:{minute:0>2}:{second:0>2}', (x, y))
        else:
            text(f'{cellContent}', (x, y))