Computational Literacies Lab

From Python to typed pseudocode

On this page
  1. Types and type signatures
  2. Typed arrays
  3. Variable-focused iteration
  4. Operators
  5. Conditionals
  6. Strings
  7. Maps
  8. Functions, parameters, and scope
  9. Objects and classes
  10. Input, output, comments, and missing values

You learned to program in Python.12 Much professional and educational writing about algorithms uses a typed pseudocode instead, and it asks you to read code that looks more like Java or C. The ideas are the same as the ones you already know, and what changes is how much gets written down. Python does several things for you without being asked: it keeps track of what kind of value each name holds, it lets a list hold anything and grow whenever you append, and it counts through a loop with range without ever showing you the counter. Typed pseudocode makes you write those things out. The differences fall into two groups, and this section takes them in that order. The first group follows from a single idea, that every variable has a declared type, and that one idea reshapes variables, function headers, arrays, and loops. These take the most getting used to, and the first three subsections cover them, each building on the one before. The second group is mostly a matter of spelling, such as elseif for elif or output for print, but it hides a few rules that genuinely differ from Python's and change what a line computes, so the later subsections point those out. Throughout, the Python you know appears beside its equivalent. Examples are numbered, because discussions of code usually refer to line numbers. Lines beginning with >>> show output, not a prompt.

Types and type signatures#

In Python, a variable is a name that can point to any value.1 Types belong to values, and the name itself has none, so the same name can hold a number on one line and a string on the next. You can check a value's type when you want to, with type(),3 but Python never asks you to state it in advance.

count = 0
count = "none"     # fine in Python
print(type(count))

Typed pseudocode reverses this arrangement. In the pseudocode, a variable has a declared type, and holds only that type. You declare the type at the first assignment and never again, so from then on you use the name alone.

01 int count = 0
02 count = 5
03 count = "none"

Line 02 is correct, because it puts a new int into a variable declared as int. Line 03 is an error, because a String can't go in an int variable. Writing int count = 5 on line 02 would also be wrong, for a different reason: repeating the type declares the variable again. A type name at the start of a line therefore tells you that a new variable is being created, and its absence tells you that an existing one is being reused, which is worth noticing whenever you read code.

There are five basic types. A bool is TRUE or FALSE, written in capitals. An int is a whole number. A float is a decimal, treated as having infinite precision, so you can reason about decimals as exact and ignore results like Python's 0.30000000000000004. A char is one character, written in double quotes; Python has no separate type for this and treats a single character as a short string. A String is written in double quotes only, where Python also accepts single quotes. The table pairs each type with a Python assignment you might write.

PythonPseudocode
done = Falsebool done = FALSE
age = 16int age = 16
gpa = 3.7float gpa = 3.7
initial = "M"char initial = "M"
name = "Ana"String name = "Ana"

Declared types matter after the declaration too, because operators check them. Arithmetic and comparison need matching types. Python quietly converts 2 * 1.5, treating the 2 as a decimal so the product can be 3.0, but here an int and a float don't combine. In practice, this means reading each line while asking whether the types match, since a mismatch is often exactly what a piece of code is meant to show you. When a program really does need to combine types, a library may offer a conversion method, which the code will define for you.

The same idea extends from variables to functions. If every variable has a type, then so does every parameter, and the function's header has to say what it is. Functions therefore have type signatures: each parameter's type appears in the header. A signature tells you, without reading the body, how to call a function correctly.

def repeat(text, times):
    result = ""
    for i in range(times):
        result = result + text
    return result

print(repeat("hi", 3))
01 function repeat(String text, int times)
02   String result = ""
03   for (int i = 0; i < times; i = i + 1)
04     result = concat(result, text)
05   return result
06
07 output(repeat("hi", 3))

Reading the two versions side by side shows the declared-type idea at work in several places at once. Line 01 names the types Python left unstated: text must be a String and times an int. Line 02 creates result, so it carries a type, while line 04 reuses it, so it doesn't. Line 03 is a counting loop of the kind explained under "Variable-focused iteration" below, and line 04 uses concat where Python used +, for the reason given under "Strings." The signature on line 01 states what the function needs, though not what it returns. repeat(3, "hi") is wrong, even though it has the right number of arguments, because the types are in the wrong order, and you can see the mistake from the signature alone, without tracing the body.

That is also why signatures matter beyond catching mistakes. A signature with no details about the body is also what makes a function an abstraction: documentation such as "round_to(float value, int digits) returns a float" is all a caller needs. The caller can use the function correctly while the body stays hidden, and the body can change without the caller noticing, an idea developed under 0002.d and 0002.e.

Typed arrays#

Declared types have their largest consequences in collections of values, and here the gap between Python and the pseudocode is widest. Python's list hides a lot.24 It can hold values of any type, it grows when you append, and you never think about where it lives in memory. An array is the structure underneath ideas like this: a row of same-size slots in memory, numbered from 0. Because every slot is the same size, a computer can find slot i immediately by arithmetic, starting at the beginning of the row and moving ahead i slots, without looking at any of the slots in between. That only works if every value takes the same amount of space, which is why an array holds one declared type, and why the type is part of its declaration.

sizes = [20, 40, 60]
mixed = [1, "two", 3.0]    # allowed in Python
sizes.append(80)
print(len(sizes))
print(sizes[0])
01 int[] sizes = [20, 40, 60]
02 sizes.append(80)
03 output(sizes.size[1])
04 output(sizes[0])
>>> 4
>>> 20

int[] means "array of int," the declared type applied to every slot at once, so a declaration such as int[] v = [1, "two", 3.0] is invalid even though Python accepts the list mixed. Indexing and .append look just as they do in Python. Length is the surprise. Note sizes.size[1]: an array's .size returns a pair, [rows, columns]. A one-dimensional array has one row, so [20, 40, 60] has a size of [1, 3] and its length is .size[1]. After line 02 appends 80, that length is 4, which is what line 03 outputs. The edges of an array are stricter than a Python list's, too. Using an index that doesn't exist gives ERROR, including -1, which Python would accept as a way to reach the last item. An array of size n has a final index of n - 1, a fact that becomes important as soon as a loop walks through the array.

The reason .size reports rows and columns becomes clear with a second dimension. Two-dimensional arrays are written with one pair of brackets, by row and then column, where Python indexes a nested list with two pairs. You may have seen this form in NumPy.5

grid = [[1, 2, 3], [4, 5, 6]]
print(grid[1][2])
01 int[][] grid = [[1, 2, 3], [4, 5, 6]]
02 output(grid[1, 2])
03 output(grid.size)
>>> 6
>>> [2,3]

The declaration int[][] has one pair of brackets for each dimension. grid[1, 2] is row 1, column 2, which, counting from 0, is the third value in the second row, 6. The grid has two rows of three values each, so its size is [2,3].

Arrays come with methods for changing and searching them. Some match Python exactly, and others differ in what they return or in where their boundaries fall.

MethodEffectPython equivalent
a.append(x)Adds to the enda.append(x)
a.insert(i, x)Adds at index i, which must already exist, shifting later values upa.insert(i, x)
a.del(i)Removes the value at index i, shifting later values downdel a[i]
a.search(x)Returns [row, column] of the first match, or NULLa.index(x)
a.slice(r1, r2, c1, c2)Returns a part; bounds are inclusivea[c1:c2 + 1], since Python excludes the end

Each difference follows from something above. Because .insert needs an index that already exists, it can't add past the end, which is the job of .append. Because an array may have two dimensions, .search reports a position as a [row, column] pair, and when nothing matches it returns NULL where Python's .index stops with an error. Because .slice includes both of its bounds, a Python slice covering the same values needs a stop one higher.

The shifting done by .insert and .del matters most inside a loop, because a loop moves through positions while these methods move values between positions. Deleting shifts later values, so a forward loop that deletes as it goes skips whatever moves into the current position: after the value at index 2 is deleted, the value that was at 3 now sits at 2, and the loop moves on to 3 without ever examining it. Appending inside a loop whose condition checks .size can make the loop run forever, since its stopping point keeps moving. Both problems come from counting through positions, and counting through positions is how every loop over an array works in the pseudocode, which brings us to loops.

Variable-focused iteration#

This is the biggest change. In Python, a for loop hands you each item, and range hides the counting.2

for size in sizes:
    print(size)

for i in range(100):
    print(i)

The typed for loop has no "for each." It is built around a counter variable instead, and like any other variable, the counter is declared with a type, which is why the header begins int i. The header states three things in parentheses: where the variable starts, the condition for continuing, and how the variable changes.

01 for (int i = 0; i < 100; i = i + 1)
02   output(i)

The three parts run at different times. The first, int i = 0, runs once, before anything else. The second, i < 100, is checked before each pass, and the body runs only while it is TRUE. The third, i = i + 1, runs after each pass, and then the condition is checked again. Many languages write the update as i++ or i += 1, so you will often see for (int i = 0; i < 100; i++). It means the same thing. This guide writes it out in full.

Since the three parts are ordinary statements about a variable, they can be pulled apart onto separate lines. The loop is exactly equivalent to this while loop:

01 int i = 0
02 while (i < 100)
03   output(i)
04   i = i + 1

Rewriting a header this way is a dependable method for working out what an unfamiliar for loop does, because it puts each part where it actually runs.

To go through an array, loop over positions rather than items, and use the position to get each item. This is where the array and the loop meet: the counter is an int index, the condition compares it with the length .size[1] from the previous subsection, and sizes[i] retrieves the item at that position.

for size in sizes:
    print(size)
01 for (int i = 0; i < sizes.size[1]; i = i + 1)
02   output(sizes[i])

Python's range forms translate directly. Note that < matches range's excluded stop: range(5) stops before 5, and so does a loop that continues while i < 5. The step becomes the update, and counting down means reversing the comparison as well as the step.

PythonPseudocode headerValues
range(5)for (int i = 0; i < 5; i = i + 1)0 1 2 3 4
range(1, 6)for (int i = 1; i < 6; i = i + 1)1 2 3 4 5
range(0, 10, 2)for (int i = 0; i < 10; i = i + 2)0 2 4 6 8
range(5, 0, -1)for (int i = 5; i > 0; i = i - 1)5 4 3 2 1

Writing the start, the condition, and the update yourself gives you control over each, and also a chance to get each wrong. Almost all errors with this loop are at its boundaries, so check four things on every loop you read. The first is whether the condition uses < or <=. i <= 5 includes 5, so for (int i = 0; i <= a.size[1]; i = i + 1) reads past the end of a: when i reaches a.size[1] it is one beyond the final index, n - 1, so a loop that outputs a[i] prints every value and then ERROR. The second is where the loop starts, since starting at 1 skips a[0]. The third is whether the update is right: if i never approaches the stopping point, the loop never ends. The fourth is whether the body reaches ahead of the counter. a[i + 1] needs i to stop at a.size[1] - 2, one earlier than usual, so that i + 1 is still a valid index.

Because the loop is counter-based, it can do things that are awkward with for x in xs: compare neighbors, go backward, or skip by twos. Comparing neighbors is exactly the situation in which the fourth check applies. Backward loops are also the safe way to delete values while looping, solving the problem described under "Typed arrays": deleting at index i shifts only the values after i, and a backward loop has already visited those. One more difference concerns what is left when the loop ends. Depending on the language, a variable declared in the header may not exist after the loop, whereas Python keeps it. This is an instance of scope, which returns under "Functions, parameters, and scope."

Since every step of the loop is written out, you can follow it by hand. Tracing means running a program by hand, keeping a table of each variable after each pass. It is the core skill for reading code, and it makes the boundary checks concrete, because the table shows the exact value of the counter at which the loop stops.

01 int total = 0
02 for (int i = 2; i < 10; i = i + 3)
03   total = total + i
04 output(total)
ii < 10?total after line 03
2yes2
5yes7
8yes15
11noloop ends

The output is >>> 15. The last row is the one to watch. The counter does reach 11, but the condition is checked before the body runs, so 11 is never added to total.

Operators#

With types, arrays, and loops in place, the remaining differences are mostly notation, and most operators look the way you would expect. A few that look familiar follow different rules, though, and several of those rules come back to declared types. The table collects the differences, and the paragraphs after it explain the ones most likely to change what a line computes.

PythonPseudocodeWhat changes
2 ** 32 ^ 3^ is always an exponent.
7 / 2 is 3.57 / 2Two int values give a rounded int, so 8 / 3 is 3. Python's // rounds down instead.
17 % 517 % 5The same: the remainder, 2.
and, or, not xAND, OR, NOT(x)Capitals, and NOT takes parentheses.
a == b, a != bThe sameWorks for any type, as long as the types match. Comparing an int and a String gives ERROR.
1 < x < 9(1 < x) AND (x < 9)Chained comparisons don't exist.
"a" < "b"Not defined<, >, <=, >= are for matched int or float pairs only.

The exponent is a change of symbol only, though one to watch, since ^ means exclusive or in Python. Division is a change of meaning, and it follows from declared types: two int values must produce an int, so the fractional part has to go somewhere. The pseudocode rounds it, so 8 / 3, which is about 2.67, gives 3, whereas Python's 8 // 3 rounds down and gives 2. The conventions don't say how a tie such as 7 / 2 is rounded. Types also explain the comparison rows. Equality works for any type because any two values of the same type can be compared, while ordering with < and > is defined only for numbers. A comparison between different types gives ERROR, just as assigning a String to an int variable does.

Arithmetic follows the usual order: parentheses, exponents, multiplication and division, addition and subtraction. a + b + c / 3 divides only c, so an average of three values needs parentheses around the sum.

Logical operators are where the pseudocode departs most from Python. AND and OR run left to right. Parentheses come first, and then there is no precedence between them. This differs from Python, where and comes first.

True or False and False    # True in Python
01 output(TRUE OR FALSE AND FALSE)
>>> FALSE

TRUE OR FALSE is TRUE, and then TRUE AND FALSE is FALSE. Python, by contrast, evaluates False and False first and gets True or False, which is True. Use parentheses to say what you mean, and when you read someone else's expression, work strictly from left to right. The same care applies to choosing between the two operators. A value within a range is tested with AND, as in (x >= 1) AND (x <= 9); OR would be true for nearly every value, since every number is either at least 1 or at most 9.

= assigns and == compares. if (x = 5) is an error, and x = x + 1 isn't an equation: it computes the right side and stores the result.

Conditionals#

Comparisons and logical operators produce the TRUE and FALSE values that conditionals test, and the conditional statement itself changes only slightly.

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
else:
    grade = "C"
01 if (score >= 90)
02   grade = "A"
03 elseif (score >= 80)
04   grade = "B"
05 else
06   grade = "C"

Conditions go in parentheses, there are no colons, and elif becomes elseif. Indentation still defines blocks, just as in Python. The logic of a chain is the same as Python's, and it rewards careful reading. Only the first true branch in a chain runs, so order matters: testing score >= 60 first would give everyone above 60 the lowest passing grade, because a score of 95 would match that test and never reach the others. Separate if statements are each checked, so if the two tests above were written as separate if statements, a score of 95 would set grade to "A" and then overwrite it with "B".

Strings#

Strings change in three ways, and each echoes something seen earlier: an attribute in place of a function, as with an array's .size; an inclusive stop index, as with .slice; and an operator restricted by type.

PythonPseudocodeWhat changes
len(text)text.lenAn attribute, so no parentheses.
text[0:4]text.sub(0, 3)The stop index is included: "They went".sub(0, 3) is "They".
"a" + "b"concat("a", "b")+ is for numbers only. concat takes any number of strings.

To translate a Python slice, then, subtract one from its stop. The published description of .sub is itself inconsistent on this point: it calls the substring "exclusive," but its own example, "They went away.".sub(0, 4), gives "They ", which includes index 4. Follow the example, and treat the stop index as included. And because + is reserved for numbers, joining strings always goes through concat, which can join several at once, as in concat("Good", " ", "morning").

Maps#

Arrays apply a declared type to every slot, and maps apply the same idea to pairs. A Python dictionary4 becomes a map or associative array, which stores values under keys instead of numbered positions. It declares the key type and then the value type, all keys share one type, and all values share one type. Curly braces are used for access, as well as for writing the map's contents.

scores = {"Ana": 8, "Ben": 6}
scores["Cy"] = 9
print(scores["Ana"])
print(len(scores))
01 String int{} scores = {"Ana": 8, "Ben": 6}
02 scores{"Cy"} = 9
03 output(scores{"Ana"})
04 output(scores.size)
>>> 8
>>> 3

Keys are unique, and assigning to a new key adds a pair, which is why line 02 raises the size from 2 to 3. A map's .size is a single int, unlike an array's pair, since a map has no rows or columns to report. .remove(key) deletes a pair, and .search(value) returns an array of matching keys. It returns an array because, although keys are unique, several keys can share a value: in scores, two students could both have 8. Declared types also limit what a map can hold. A mixed dictionary like a game's state6 isn't possible, because its values would have different types. Reading a key that doesn't exist is an error.

Functions, parameters, and scope#

Type signatures showed how a function is called. What happens to values as they pass into a function and back out works the same way it does in Python.

def add_tax(price):
    price = price + 5
    return price

cost = 20
add_tax(cost)
print(cost)
cost = add_tax(cost)
print(cost)
01 function add_tax(int price)
02   price = price + 5
03   return price
04
05 int cost = 20
06 add_tax(cost)
07 output(cost)
08 cost = add_tax(cost)
09 output(cost)
>>> 20
>>> 25

As in Python,17 changing a parameter doesn't change the caller's variable. The parameter price is a separate variable that starts with the value of cost, so line 02 changes price and leaves cost alone. Line 06 calls the function but throws away its return value, so line 07 still outputs 20. Only line 08, which stores the returned value in cost, produces 25. This rests on the difference between output() and return: output shows a value, while return hands it back to the caller, which can store it, use it, or ignore it. A function with no return is a different matter. Python gives such a function the value None,7 but the pseudocode conventions don't say what it gives, so code should never rely on the result of one.

The separation between price and cost is one case of a general rule. A variable declared inside a function or block has scope limited to it, so it isn't available outside it, and a function's local variable is created anew on each call, so it keeps nothing from the call before. The loop counter that may vanish after its loop, described under "Variable-focused iteration," follows the same rule, since the loop is a block.

Objects and classes#

Objects extend declared types one step further. Dot notation is the same as in Python.8 Creating an object often looks like a typed declaration, with the class name in the position of the type, so a class works as a type beyond the five basic ones. Attributes and methods work as you would expect. Classes defined by a program, or provided by a library, are explained in the code that uses them, often in a comment.

d = Die()
d.roll()
print(d.face)
01 Die d = Die()
02 d.roll()
03 output(d.face)

Methods can also be called on a class, as in Math.random(), when a library documents them that way.9 The ideas behind classes, such as encapsulation and inheritance, are taken up under 0002.l.

Input, output, comments, and missing values#

The last group of differences concerns how a program exchanges values with the outside world, how it carries notes for human readers, and how it signals that something is missing or has gone wrong.

PythonPseudocode
print(x)output(x)
input()input(), which can be assigned to a typed variable, as in int i = input()
# comment/* comment */, which may span lines
type(x)type(x), which gives the type's name as a String
NoneNULL, as when a search finds nothing
An exceptionERROR, for invalid or undefined values such as dividing by zero

The last two rows are worth keeping apart, because they mean different things. NULL is an ordinary result that means "nothing here," as when .search finds no match, and a program can check for it and carry on. ERROR means that an operation itself was invalid, as with dividing by zero, an index past the end of an array, a missing map key, or a comparison between mismatched types. Many of the rules in this section, from declared types to loop boundaries, are ways of predicting where ERROR will occur.

Code also comes with a few layout conventions. A blank numbered line or a comment such as /* execute selected code here */ marks a slot where code belongs. A long line may wrap onto the next line number without being a new statement.

The conventions don't cover everything a program might contain. They don't define class definitions or class attributes, the classes and methods of a library, the methods of a hypothetical robot such as r.forward(2), or break, and they don't spell out scope rules or say whether negative indexes work. Code that uses any of these explains it, usually in the setup or a comment, so read that setup carefully, and don't assume that Python's behavior carries over: Python keeps a loop variable after the loop ends and accepts numbers[-1], and neither can be counted on here. Code written by others doesn't always follow the conventions exactly, either. You may see else if for elseif, str or string for String, .size used as a simple length, a declaration with no value, = inside a condition, uneven indentation, inconsistent capitalization of a name, or a stray symbol. Treat such differences as noise unless the question turns on them.

If you remember only a few things from this section, make them the rules that change what a line computes: ^ is an exponent, >>> marks output, AND and OR run strictly left to right, two int values divide to a rounded int, an array's .size is a pair while a map's is a single number, every loop boundary deserves a check of < against <=, and declared types are a tool for reasoning, since a line that mixes an int and a float is often the point of the code. These conventions are the notation for the code that appears throughout the rest of this competency. The sections that follow turn from how code is written to what programs are for and how they are designed, beginning with programs whose purpose is to represent something outside the computer.

  • declared type: the type stated when a variable is created; the variable can hold only values of that type
  • type signature: a function header that lists the type of each parameter, showing how to call the function without reading its body
  • array: a numbered row of same-size slots holding values of one declared type, indexed from 0
  • two-dimensional array: an array indexed by row and then column, as in grid[1, 2]
  • map (associative array): a collection of key-value pairs with unique keys, one key type, and one value type
  • tracing: running a program by hand, recording each variable's value after each step
  • scope: the part of a program in which a variable exists, limited to the function or block that declares it
  • NULL: a value meaning that nothing was found
  • ERROR: the result of an invalid or undefined operation
  1. MWC1, Names: variables, values, and functions with arguments. ↩ ↩2 ↩3

  2. MWC1, Iteration: lists, for loops, and range. ↩ ↩2 ↩3

  3. MWC1, Number Words: top-down planning, bottom-up implementation, and types. ↩

  4. MWC1, Weather: dictionaries, web APIs, and distributed services. ↩ ↩2

  5. MWC2, Classification: Neural: neural networks, backpropagation, and NumPy-style array indexing. ↩

  6. MWC1, Tic Tac Toe: game state stored in a dictionary. ↩

  7. MWC1, Scatter: top-down and bottom-up problem solving, conditionals, return values, minimums and maximums, and plots. ↩ ↩2

  8. MWC1, Dice: classes, instances, randomness, and repeated runs. ↩

  9. MWC2, Sub Rosa: public and private keys in a messaging system. ↩