Wednesday, October 15th, 2025¶

Project 3: Tartans (continued)¶

Last week, we worked on generating tartans from a given recipe. We considered the following example of a tartan recipe:

Pattern :

B14 K6 B6 K6 B6 K32 OG32

where the colors B, K, and OG are given by the RGB triples:

B : [52, 80, 100]
K : [16, 16, 16]
OG : [92, 100, 40]

We talked last week about how to use Python to algorithmically create the vertical_stripes array. Our approach depended on two lists, colors and widths, which we defined based on the tartan recipe.

In [20]:
B = [52, 80, 100]
K = [16, 16, 16]
OG = [92, 100, 40]

colors = [B, K, B, K, B, K, OG]
widths = [14, 6, 6, 6, 6, 32, 32]

This is a reasonable way to represent our tartan pattern in Python, but it still required us to carefully enter the correct sequence of colors and corresponding widths. It would be nice if we could use Python to process the tartan recipe directly to minimize the amount of manual work. As a starting point, let's define a string storing the tartan recipe:

In [2]:
tartan_recipe = 'B14 K6 B6 K6 B6 K32 OG32'

Can we use Python to generate the colors and widths lists directly from the tartan_recipe string?

String processing¶

We have already seen some useful tools for working with strings in Python. For example:

  • The .join method allows us to concatenate a list of strings using a given string as a separator.
  • The .format method allows us to plug values into a string template.

Another helpful tool is the .split method, which allows us to separate a string into a list of substrings. The syntax is something like:

some_string.split(some_separator)

This will separate the some_string string into a list of substrings, where each separation occurs whenever the some_separator string is found within some_string.

In [4]:
my_string = 'Hello, this is a test string.'
my_separator = 'is'

print(my_string.split(my_separator))
['Hello, th', ' ', ' a test string.']

Note: If we call the .split method with no input variable, by default the string will be split anywhere there is white space (e.g. spaces, tabs, line breaks, etc.).

In [5]:
print(my_string.split())
['Hello,', 'this', 'is', 'a', 'test', 'string.']

We can then apply the .split method to the tartan_recipe string to get a sequence of substrings, each of which represents a particular stripe.

In [6]:
stripe_recipes = tartan_recipe.split()
print(stripe_recipes)
['B14', 'K6', 'B6', 'K6', 'B6', 'K32', 'OG32']

Can we iterate through each of these stripe recipes and pick out the color and width?

In [7]:
colors = []
widths = []

for stripe_recipe in stripe_recipes:
    color = stripe_recipe[0]
    width = stripe_recipe[1:]
    colors.append(color)
    widths.append(width)

print(colors)
print(widths)
['B', 'K', 'B', 'K', 'B', 'K', 'O']
['14', '6', '6', '6', '6', '32', 'G32']

Problem: We can't just take the first character from stripe_recipe as the color code, because some color codes have two characters (e.g. 'OG').

It would be very helpful if we could pick out which parts of a string are letters and which parts of a string are numbers. The following methods (called on a string) can help with this:

  • The .isalpha method will return True if the string contains only alphabetic characters (i.e. letters).
  • The .isnumeric method will return True if the string contains only numeric characters (i.e. numbers).
In [9]:
my_string = 'Hello 1234 this 567 string 8910 contains letters and 4321 numbers.'

alpha_chars = []

for char in my_string:
    if char.isnumeric():
        print(char)
    if char.isalpha():
        alpha_chars.append(char)
1
2
3
4
5
6
7
8
9
1
0
4
3
2
1
In [10]:
print(alpha_chars)
['H', 'e', 'l', 'l', 'o', 't', 'h', 'i', 's', 's', 't', 'r', 'i', 'n', 'g', 'c', 'o', 'n', 't', 'a', 'i', 'n', 's', 'l', 'e', 't', 't', 'e', 'r', 's', 'a', 'n', 'd', 'n', 'u', 'm', 'b', 'e', 'r', 's']
In [11]:
print(''.join(alpha_chars))
Hellothisstringcontainslettersandnumbers

Exercise: Write a function that takes in a tartan recipe string and returns a list of color codes (e.g. ['B', 'K', 'B', ...]) and widths.

In [22]:
color_codes = []
widths = []

for stripe_recipe in stripe_recipes:
    color_code = ''
    width_str = ''
    for char in stripe_recipe:
        if char.isalpha():
            color_code = color_code + char
        if char.isnumeric():
            width_str = width_str + char
    color_codes.append(color_code)
    widths.append(int(width_str))

print(color_codes)
print(widths)
['B', 'K', 'B', 'K', 'B', 'K', 'OG']
[14, 6, 6, 6, 6, 32, 32]

Let's try to rewrite this using list comprehension:

In [23]:
color_codes = []
widths = []

for stripe_recipe in stripe_recipes:
    color_code = ''.join([char for char in stripe_recipe if char.isalpha()])
    width_str = ''.join([char for char in stripe_recipe if char.isnumeric()])
    color_codes.append(color_code)
    widths.append(int(width_str))

print(color_codes)
print(widths)
['B', 'K', 'B', 'K', 'B', 'K', 'OG']
[14, 6, 6, 6, 6, 32, 32]

From the above, we can pick out the color code strings from the tartan recipe. On the other hand, in order to generate our vertical_stripes array, we need to get the corresponding RGB triples (not just the color code strings). How can accomplish this?

In [27]:
print(B)
print(K)
print(OG)
[52, 80, 100]
[16, 16, 16]
[92, 100, 40]
In [29]:
color_codes = ['B', 'K', 'B', 'K', 'B', 'K', 'OG']

colors = []
for color_code in color_codes:
    if color_code == 'B':
        colors.append(B)
    elif color_code == 'K':
        colors.append(K)
    elif color_code == 'OG':
        colors.append(OG)
print(colors)  
[[52, 80, 100], [16, 16, 16], [52, 80, 100], [16, 16, 16], [52, 80, 100], [16, 16, 16], [92, 100, 40]]

It would be convenient if we could define a mapping that maps each color code to its respective RGB triple.

Dictionaries¶

Let's consider a list, my_list:

In [30]:
my_list = ['Hello', 'World', 'Goodbye', 7]

We can think of my_list as a mapping between indices 0, 1, 2, 3, and values 'Hello', 'World', 'Goodbye', 7. That is, we have the following mappings:

  • index 0 maps to value 'Hello',
  • index 1 maps to value 'World',
  • index 2 maps to value 'Goodbye',
  • index 3 maps to value 7.

To retrieve values of a list, we can "plug in" an index using square brackets []:

In [31]:
print(my_list[0])
Hello
In [32]:
print(my_list[3])
7

A Python dictionary works very similarly, except that instead of a map from indices to values, we have a map from keys to values. These keys do not need to be integers 0, 1, 2, ..., but are more flexible. Dictionary keys can include integers, floats, strings, tuples, functions, and many other types (anything that is immutable).

To define a dictionary, we can use curly braces {} containing a comma-separated sequence of <key>:<value> mappings. For example, suppose we want to construct a dictionary with the following mappings:

  • key 5 maps to value 'Hello!',
  • key (0,255,0) maps to 'Green',
  • key 'Good' maps to value 10,
  • key 'Bad' maps to value 0,
  • key 'Red' maps to value (255,0,0).
In [34]:
my_dictionary = {5: 'Hello!', 
                 (0,255,0): 'Green', 
                 'Good': 10, 
                 'Bad': 0, 
                 'Red': (255,0,0)}

Just like with lists, we can "plug in" a key to the dictionary and retrieve the corresponding value using square brackets [].

In [35]:
my_dictionary['Red']
Out[35]:
(255, 0, 0)

Note: unlike lists , we cannot use slicing with dictionaries. This is because the dictionaries do not have any inherent order in Python.

If desired, we can access the keys of a dictionary using the .keys method. Similarly, we can access the values using the .values method. Finally, we can access key/value pairs using the .items method.

In [36]:
my_dictionary.keys()
Out[36]:
dict_keys([5, (0, 255, 0), 'Good', 'Bad', 'Red'])
In [37]:
my_dictionary.values()
Out[37]:
dict_values(['Hello!', 'Green', 10, 0, (255, 0, 0)])
In [38]:
my_dictionary.items()
Out[38]:
dict_items([(5, 'Hello!'), ((0, 255, 0), 'Green'), ('Good', 10), ('Bad', 0), ('Red', (255, 0, 0))])

This allows us to iterate through dictionaries in several ways:

  • for key in my_dictionary.keys() will iterate through each key of the dictionary. We can also write for key in my_dictionary, which will still iterate through the keys.
  • for value in my_dictionary.values() will iterate through each value of the dictionary.
  • for key, value in my_dictionary.items() will iterate through each key/value pair.
In [39]:
for key in my_dictionary:
    print('Key:', key)
    print('Value:', my_dictionary[key])
    print()
Key: 5
Value: Hello!

Key: (0, 255, 0)
Value: Green

Key: Good
Value: 10

Key: Bad
Value: 0

Key: Red
Value: (255, 0, 0)

In [40]:
for key, value in my_dictionary.items():
    print('Key:', key)
    print('Value:', value)
    print()
Key: 5
Value: Hello!

Key: (0, 255, 0)
Value: Green

Key: Good
Value: 10

Key: Bad
Value: 0

Key: Red
Value: (255, 0, 0)

Exercise: Write a dictionary whose keys are the color codes 'B', 'K', and 'OG' and whose values are the corresponding RGB triples.

In [ ]:
 

Exercise: Use the color_codes list and the above dictionary to construct a list colors containing the RGB triples for each stripe in the tartan recipe.

In [ ]:
 

Other examples where dictionaries may be useful:

In [44]:
my_bio_data = {'first name': 'Jonathan',
               'last name': 'Lottes',
               'age': 36,
               'city': 'Buffalo'}

Bonnie_bio_data = {'first name': 'Bonnie',
                   'last name': 'Lottes',
                   'age': 34,
                   'city': 'Buffalo'}

bio_data = {'Jonathan Lottes': my_bio_data,
            'Bonnie Lottes': Bonnie_bio_data}
In [46]:
bio_data['Jonathan Lottes']['age']
Out[46]:
36

Remaining tasks to be addressed for Project 3:¶

  • We have discussed how to combine the vertical_stripes and horizontal_stripes arrays in a checkerboard pattern. We will need to use the more authentic pattern described on the project page to combine these arrays to generate our tartan image.
  • We are told to recreate a tartan image that is 500 rows by 500 columns. On the other hand, the tartan recipe does not add up to a total width of 500. We will need to think about how to repeat the pattern to fill the entire (500, 500, 3) array.
In [49]:
recipe = '''T4 W44 T40 LT6 T6 LT6 T6 LT48 T6 LT6 T6 LT6 T40 W44 T4
T : [96, 64, 0]
W : [224, 224, 224]
LT : [160, 136, 88]'''
In [50]:
recipe.split('\n')
Out[50]:
['T4 W44 T40 LT6 T6 LT6 T6 LT48 T6 LT6 T6 LT6 T40 W44 T4',
 'T : [96, 64, 0]',
 'W : [224, 224, 224]',
 'LT : [160, 136, 88]']