PP(MC)

  1. Table of contents
  2. Getting started (previous)
  3. Functions and methods (next)

Built-in data types

  1. Top
  2. Numeric types
  3. Variables
  4. Container types
  5. Exercises
  6. References

Interludes

  1. Operator terminology
  2. Naming variables

Built-in data types

An important part of writing programs is deciding on the proper representations of the data concerned. When composing music, the data we are concerned with are notes, measures, frequency spectrums, pitch-class sets, instrumentation, and so on. Deciding on the proper representation might mean different things in different contexts. Do we need a particular data model to : keep the data it holds sorted ? ensure that every element it contains is unique ? make each element it contains accessible via a unique key ?

Python offers a number of data types built into every installation. These types are, in a way, abstract until you give them meaning. Along the course of your reading you will learn to extend these types, and even to create types of your own, but in this section we will explore a number (nearly all) of those types that are built-in with Python.

We will also have a look at a few basic arithmetic operators, giving us the necessary material with which examples may be demonstrated.

Numeric types

int

Integers are whole numbers, either negative, zero, or positive, that have no decimal part. There is an infinite number of them in either direction.

In Python, integers are expressed as a number without a decimal. Like this :

>>> 42
42

float

Floats, or floating-point numbers, are a little more complicated. As you are most probably aware, computer hardware can only represent things in binary form (zeros and ones) and this makes it difficult to represent decimal values precisely. Understanding binary representations of decimal numbers (superficially) is neither difficult nor uninteresting, but it is tangential to this course's aims. I'll hand the interested a line with this link.

So to get back to our definition : floats are real (rational and irrational) numbers represented as accurately as your computer's architecture can handle.

In Python, floats are expressed as a number with a decimal value. Like this :

>>> 3.14159265359
3.14159265359

As a side note, there is also an infinite number of real numbers in either direction, but is that infinite number greater than the infinite number of integers ? Aleph number.

Arithmetic

You'll probably want to do something with these numbers now. You'll find arithmetic is perfectly straight-forward in Python and should appear familiar to anyone who's ever come across a calculator.

The following table lists some common operators and their operator precedence (more on that very soon) :

Order Operation Operator Example
0 exponent **
>>> 6.48074069840786 ** 2
42.0
1 negation -
>>> -84 / -2
42.0
2 product *
>>> 21 * 2
42
2 quotient /
>>> 84 / 2
42
2 floored quotient //
>>> 85 // 2
42
2 remainder of %
>>> 92 % 50
42
3 sum +
>>> 40 + 2
42
3 difference -
>>> 44 - 2
42

Operator precedence

An expression can be composed of several operators and operands. The order in which the operators are evaluated is determined first by their operator precedence, then left to right.

Here's a simple example :

>>> -2 + 4 * 11
42

In the preceding example the following operations take place :

  1. -2      = -2 (remember that - is the negation operator)
  2.  4 * 11 = 44
  3. -2 + 44 = 42

Operators that are of the same order of precedence are evaluated left to right.

>>> 43 % 22 // 10 * 21
42

In the preceding example the following operations take place :

  1. 43 %  22 = 21
  2. 21 // 10 = 2 
  3.  2 *  21 = 42

Order can be broken by using parentheses to group operations.

>>> (-2 + 4) * 11
22

In the preceding example the following operations take place :

  1. (-2 + 4)
    1. -2     = -2
    2. -2 + 4 =  2 
  2. 2 * 11 = 22

Official docs
Operator terminology

An operand is a thing being operated on.

An operator can operate on one operand and be called a unary operator, or two operands, in which case it is called a binary operator. There are also ternary operators that operate on three operands but we haven't met any of those yet.

Operators are said to be infix when their operands appear on either side of it. Most operators are of this kind :

  • x + y
  • x * y
  • etc...

Operators are said to be prefix when they precede their operand. The negation operator is an example of this.

Operators are said to be postfix when they (you guessed it) follow their operand. We haven't yet seen any examples of these.

Type conversions (implicit casting)

It can happen that the resulting value of an operation be of a different type from one of the operands used in the operation. Here's an example to illustrate :

>>> 4 / 1.6
2.5

One of the operands is an int while the other is a float. The result is a float.

Here's another example :

>>> 12 // 5
2

This time we are using a floor quotient so the result's decimal part is 0, both operands are int and the resulting value is also an int.

A final example :

>>> 4 // 1.6
2.0

Again we are using a floor quotient so the result's decimal part is still 0, but the resulting value this time is a float.

Here's the point : The resulting value will always be of the same type as the operand with the highest precision.

Variables

It is often useful to assign a name to the value returned by an expression, so that it can be clear to anyone reading your code what that value represents and so that this value can be used again without resorting to the full expression a second time. Doing this is called variable assignment. The (infix) assignment operator in Python is the = (equal sign). Its first (left) operand is the variable's name of your choosing and its second (right) operator is an expression. Here's an example :

>>> midi_c = 60
>>> midi_g = 67
>>> semitones = midi_g - midi_c
>>> semitones
7

You can also assign a new value to an existing variable, overwriting the previous one :

>>> midi_c = 48
>>> midi_g - midi_c
19

Naming variables

By PEP-8 convention variable names are "lowercase, with words separated by underscores". They should clearly represent what they refer to. Some would say they should be at least three characters long. Following this advice - and it is nothing more than advice because your code will run regardless - will make it easier for those reading your code to follow along. That includes yourself 10 months down the road when you've forgotten (and you will) what you were thinking when you wrote it.

Also, Python reserves a list of names that you most definitely shouldn't use. If you do, you risk breaking their intended functionality. The full list can be found here :

False, None, True, and, as, assert, async, await, break, class, continue, def, del, elif, else, except, finally, for, from, global, if, import, in, is, lambda, nonlocal, not, or, pass, raise, return, try, while, with, yield

Container types

As the name suggests, containers are types that can contain other objects. There are several container types, each defined by a certain set of characteristics. Here's the vocabulary that describes those characteristics :

Sequence

A finite set of items indexed by non-negative integers (0, 1, 2, ...). Indices are zero-indexed, which means that they always begin at 0, which implies for example that the second item in a sequence has index number 1 (not 2).

We can refer to an item in a sequence using the indexing syntax : seq[i] for a sequence named seq and a value at index i. A slice of a sequence can be referred to using the slicing syntax : seq[i:j]. This will refer to a part of the sequence containing the items between index i and up to but not including index j. If i is left blank it defaults to 0, and if j is left blank it defaults to the index of the last item in the sequence + 1. Finally, some sequences support extended slicing where: seq[i:j:k] refers to a slice containing the items between index i and up to but not including index j in steps of k.

If you're struggling with this don't worry, there are plenty of examples coming up.

Ordered

You can count on an ordered container maintaining the order of the objects it contains as you create it, and as you manipulate their order within. An unordered container will be ordered on creation in a way that maximizes performance, but that ignores its initial ordering or any attempts to impose an order upon it.

Mutability

The objects contained by an immutable container cannot be changed after the container has been created. This also means that the length of an immutable container cannot be changed after it has been created. That said, if an item contained in an immutable container is itself mutable, it can be modified, but not removed.

Again, there are examples forthcoming.

Hashability

A hash, in computer science, is the numerical result of a (hash) function which always returns the same value for the same given input (that is to say it is deterministic). Hashing can be used to more efficiently represent and store data and to confirm equality between objects (by comparing their hashes). This second characteristic, confirming equality, is what makes hashable objects relevant to container types, as we will soon see.

list

A mutable sequence of arbitrary objects.

To start with, let's have a look at the most straight-forward container : the list. It is a finite set of indexed items (a sequence), it can contain any type of object (arbitrary objects), and it can be modified after it has been created (it is mutable). A list is created using [ ] (square brackets) surrounding items separated by , (commas).

And finally, an example. Here, a list containing seven items (MIDI notes represented by integers) is created and assigned to a variable named melody :

>>> melody = [60, 60, 64, 64, 62, 64, 62]

If this melody is in C major, and we want to change its mode to minor, we can change the offending major thirds by referring to their indices and assigning the new value 63 (E flat) like so :

>>> melody[2:4] = [63, 63]
>>> melody[-2] = 63
>>> melody
[60, 60, 63, 63, 62, 63, 62]

Don't panic. I know this example isn't trivial for you at this stage, so let's break it down.

Our original list, [60, 60, 64, 64, 62, 64, 62], has major thirds (the MIDI note E integer 64) at indices 2, 3, and 5. Remember that we can refer to a slice of a sequence by using the seq[i:j] syntax. So, we can refer to the first two Es like this :

>>> melody[2:4]
[64, 64]

The important thing to notice is that the value that is returned here is itself a list, which you can be certain of because it is a set of items, separated by commas, surrounded by square brackets. So let's note : a slice of a list is itself a list. To replace the values in a slice, we'll have to provide a slice of the same size, in our case : a list containing two items. This is what we did when we assigned the list [63, 63] to the slice melody[2:4].

Let's now look for the third E in the melody. It is the item at index 5, so we can refer to it like this :

>>> melody[5]
64

Notice that this time we got a simple integer, which is what the item at index 5 is. But in the example above I didn't use index 5, I used -2. What's that about ? Well, I wanted to show you a trick : you can also count back from the end of the sequence using negative numbers. With this trick, even if you don't know how long the sequence is, you can always get the last item using seq[-1], or the second to last using seq[-2], and so on. We then have :

>>> melody[-2]
64

On to the next container.

tuple

An immutable sequence of arbitrary objects.

From your recently acquired vocabulary and the above description, you can deduce that tuples are containers with finite and fixed lengths (immutable), who's items can be referred to by means of an indexing syntax (sequence), and that can contain arbitrary types of objects (of arbitrary objects). But that's probably still rather theoretical at the moment, so here are some practical examples.

A tuple is created using ( ) (parentheses) surrounding items separated by , (commas). Let's create a tuple of pitch classes representing a musical tonic (root) major triad :

>>> tonic_major = (0, 4, 7)

You can retrieve the items from the tuple using the indexing syntax like so :

>>> tonic_major[0]
0

>>> tonic_major[3]
7

Because our tonic_major tuple is immutable, we could now never extend, nor could we ever change the values of, the items it contains. This works great for triads : by definition they are always composed of three pitch classes and the values of those pitch classes are the triad's unalterable identities. We could, then, continue defining our triads like so :

>>> dominant = (2, 7, 11)
>>> neapolitan_sixth = (1, 5, 8)

It is sometimes useful to break a tuple's elements out and assign a variable for each of them. It is much clearer, for example, to use a variable named low_voice than to refer to tonic_major[0]. There's a quick way to assign variables in this way, it's called tuple unpacking and it's done like this :

>>> low_voice, mid_voice, high_voice = tonic_major

Now you can refer to each voice using its own variable :

>>> mid_voice
4

Note that if you use this trick, you have to assign all the values contained in the tuple.

A final thing to note about tuples is the special syntax needed to create a tuple containing only a single value. Because the Python interpreter needs to be able to distinguish between a tuple containing a single value and parentheses used to single out an expression, defining a singleton tuple requires a comma be placed after the singleton value, like so :

>>> singleton = (60, )

Now let's clear up a characteristic we've been ignoring.

"of arbitrary objects"

Now what about that last characteristic, "of arbitrary objects" ? Both lists and tuples can contain arbitrary objects (that is to say objects of any type) and any combination of these object types. So far, we've seen : int, float, list, and tuple types. To illustrate by way of an example, here is a tuple containing an arbitrary combination of all the types we've seen so far :

>>> measure = (90.0, 8, (4, 4), [60, 60, 64, 64, 62, 64, 62], (0, 4, 7))

First a float, then an int, a tuple, a list, and finally another tuple. This could work as a simple model for a musical measure, which we could unpack like so :

>>> tempo, tempo_div, time_sig, melody, accompaniment = measure

Here's another example using a list and the tuples we defined earlier :

>>> progression = [tonic_major, neapolitan_sixth, dominant, tonic_major]

The indexing syntax is still applicable to sequences contained within other sequences. As an example, we can fetch the third note of the melody contained in the measure we defined above like so :

>>> measure[3][2]
64

To clarify : measure[3] refers to the list [60, 60, 64, 64, 62, 64, 62], which is at index 3 of measure. Adding the second index [2] refers to the item at index 2 of the list at index 3 of measure, which is our note.

str

An immutable sequence of characters.

Strings are immutable (cannot be changed once created) sequences (we can refer to their items using the indexing syntax) of alpha-numeric characters. They are formed by surrounding a set of characters by either " " (double quotation marks) or ' ' (single quotation marks). Long strings spanning several lines can be formed by surrounding characters by """ """ (triple double quotation marks) or ''' ''' (triple single quotation marks).

They could, for example, represent the name of a composer :

>>> composer = "Arvo Pärt"

Or the lyrics to a song :

>>> lyrics = """
    My heart's in the Highlands, my heart is not here
    My heart's in the Highlands, a-chasing the deer
    A-chasing the wild deer, and following the roe
    My heart's in the Highlands wherever I go.
    Farewell to the Highlands, farewell to the North
    The birthplace of valor, the country of worth
    Wherever I wander, wherever I rove
    The hills of the Highlands for ever I love.
    Farewell to the mountains high cover'd with snow
    Farewell to the straths and green valleys below
    Farewell to the forests and wild-hanging woods
    Farewell to the torrents and loud-pouring floods."""

You might be wondering if there are reasons for using either " " or ' ' when creating strings. The simple fact of the matter is that either will work, but you'll have to deal with quotes inside your string differently in each case. If your string includes a ' character, you can deal with it in one of two ways : either use " " to create your string or escape the inline ' so that the interpreter can recognize that it does not represent the end of your string. Here's an illustration :

>>> line = 'My heart's in the Highlands'

This will confuse the interpreter. The string is formed using ' ', but by the end of the second word (heart) a closing ' is found and the interpreter stumbles on an s which it tries and fails to interpret. The program stops running with an error. The first way to fix this is to use " " to form the string, like so :

>>> line = "My heart's in the Highlands"

The second way is to escape the ' using the special character \ like so :

>>> line = 'My heart\'s in the Highlands'

The \ (backslash) special character warns the interpreter that the character immediately following it should be interpreted in a special way. There are several characters which can follow the \ that have special meaning. Here's is a table listing some of these characters - which along with the backslash are called escape sequences - and their functions :

Sequence Function Example
\' single quote
'My heart\'s in the Highlands'
My heart's in the Highlands
\" double quote
"Alberich : \"so verfluch ich die Liebe !\""
Alberich : "so verfluch ich die Liebe !"
\n new line
"My heart's in the Highlands,\nmy heart is not here"
My heart's in the Highlands,
my heart is not here
\t tab
"Alberich :\tso verfluch ich die Liebe !"
Alberich :	so verfluch ich die Liebe !
\\ backslash
"My heart's in the Highlands, \\ my heart is not here"
My heart's in the Highlands, \ my heart is not here

Just like any sequence, the items (characters) of a string can be drawn from it using the indexing syntax like so :

>>> lyrics[-7:-1]
'floods'

Notice that escape sequences count as one character :

>>> lyrics[0:9]
'\nMy heart'

set

An unordered set of unique immutable objects.

Sets are not sequences, the items they contain are not ordered, and while they themselves are mutable, the items they contain must be immutable. Sets are particular in that they are the only container type who's contents are strictly unique. If you have any memory of studying Venn diagrams in school (or a deeper understanding of set theory), then you have an idea what sets are for. Finding intersections, differences, and unions of sets is trivial with this data type. We'll be exploring the use of sets further on (quite soon) in this course, but in the mean time let's have a look at an example of creating one :

>>> the_five = {
    "Mily Balakirev",
    "César Cui",
    "Modest Mussorgsky",
    "Nikolai Rimsky-Korsakov",
    "Alexander Borodin"
}

dict

An unordered mutable mapping of key:value pairs.

Dictionaries map keys to values. Keys must be unique and can be of any hashable type. Values can be of any type and do not need to be unique. Of the types covered in this section, list, set, and dict cannot be hashed and cannot be used as keys. Did you notice that these types all happen to be mutable ?

The syntax for creating a dictionary is to wrap in { } (curly braces) a set of key : (colon) value pairs separated by , (commas). Here's an example :

>>> interval_semitones = {"m3": 3, "P5": 7, "M6": 9}

We've defined a dictionary who's keys are strings of common interval name abbreviations and who's values are integers representing the number of semitones in that interval.

To fetch a value from a dictionary using a key, the following syntax is used :

>>> interval_semitones["P5"]
7

Here's an example using integers as keys :

>>> interval_names = {3: "m3", 7: "P5", 9: "M6"}
>>> interval_names[9]
'M6'

Finally, here's a larger example, a dictionary containing dictionaries :

>>> strings = {
        "violin": {
            "range": ("G3", "A7"),
            "clef": "treble",
            "transposition": 0
        },
        "viola": {
            "range": ("C3", "E6"),
            "clef": "alto",
            "transposition": 0
        },
        "cello": {
            "range": ("C2", "C6"),
            "clef": "bass",
            "transposition": 0
        },
        "double bass": {
            "range": ("C2", "C5"),
            "clef": "bass",
            "transposition": -12
        }
    }

And an example of referring to the values in an internal dict :

>>> strings["viola"]["range"]
('C3', 'E6')

Arithmetic operators on container types

Exercises

  1. Which data type, or combination of types, would you choose to represent each of the following ?

    • A MIDI note number
    • A chord
    • A frequency in Hertz
    • A tempo
    • A time-signature
    • A pitch class set
    • A chord progression
    • A composition for four instruments
    • A set complex (twelve-tone matrix)

  2. Which data type would you choose to create a mapping between conventional interval abbreviations (eg.: 'P4', 'm3', etc.) and number of semitones ? Create it and use it to create a seventh chord on a MIDI note root of your choice.

  3. Calculate the pitch classes (with C being pitch class 0) of the following notes using Python's arithmetic operators. Hint : There are 12 pitch-classes (from 0 to 11) per octave.

    • MIDI note 42
    • A perfect fifth above MIDI note 50
    • A perfect fourth below middle C

  4. Translate the following musical excerpt (or another of your choosing) to Python, using any combination of data-types.

    ces Wen looked las Good = 120 King out,

References

These reference sections will normally link to heavy reading. The links are meant to help you dig deeper into the topics introduced here once you've built up your foundational skills. Don't be discouraged by their contents, return to them again later.

The official reference for this chapter is the documentation page on the Python data model.