---
title: 'Python Is Not an Acceptable ML'
description: 'Balagopal Komarath argues that Python is a poor fit for introductory programming when compared with languages that catch more mistakes early.'
date: '2022-11-28'
author:
  name: 'Balagopal Komarath'
  image: '/favicon/web-app-manifest-192x192.png'
tags: ['Programming Languages', 'Teaching', 'OCaml', 'Python']
coverImage: '/images/course-resources/theory-of-computing.png'
---

Python is a popular fixture in introductory programming courses. Its adoption is mainly driven by two reasons: its popularity in scientific computing and industry, and its perceived readability.

The first point is irrefutable. The second is more complicated. Python's surface-level readability does not compensate for its violations of several fundamental principles of programming-language and user-interface design. Many of the problems below are faced most sharply by beginning programmers. A language with better design, such as OCaml, avoids or even prevents several of them.

When programming, errors can be detected at different stages. Some are caught by an editor or IDE while typing; some by a compiler; some always appear when the program runs; and some remain hidden until particular runtime conditions occur. A well-designed programming language should detect as many errors as possible as early as possible.

Can we create a perfect language where all errors are compile-time errors? Such languages exist, but they usually require advanced knowledge to use well. Every language is designed around a tradeoff between correctness and usability. But the tradeoff is not linear. If we quantified usability and correctness on a scale of 1 to 100, some languages might be 80 in usability and 80 in correctness, while others might be 90 in usability and 50 in correctness.

My thesis is that OCaml is an 80-usability, 80-correctness language for introductory programming, while Python is, at best, a 90-usability, 50-correctness language. The examples below explain why.

## Errors of Commission and Omission

The following Python program has an error. Can you find it?

```python
def sound(animal):
    if animal == 'dog':
        return 'bow'
    elif animal == 'cat':
        return 'meow'
    elif animal == 'cow':
        return 'moo'
    elif anima1 == 'pig':
        return 'oink'
    elif animal == 'human':
        return 'huh'

print(sound('cat'))
```

It prints:

```text
meow
```

The error is that one occurrence of `animal` is misspelled as `anima1`. Python accepts the program and the bug remains hidden unless that branch is reached. This is a conditional runtime error that should have been a compilation error.

In OCaml, the same function can be written without even spelling out the parameter:

```ocaml
let sound = function
  | "dog" -> "bow"
  | "cat" -> "meow"
  | "cow" -> "moo"
  | "pig" -> "oink"
  | "human" -> "huh"
  | _ -> assert false

let () = print_endline (sound "cat")
```

The parameter name conveys no useful information here, so the language lets us avoid it. If we write the function in a more Python-like style and make the same spelling mistake, OCaml reports it immediately:

```ocaml
let sound animal =
  if animal = "dog" then "bow"
  else if animal = "cat" then "meow"
  else if animal = "cow" then "moo"
  else if anima1 = "pig" then "oink"
  else if animal = "human" then "huh"
  else assert false
```

```text
Line 5, characters 10-16:
5 |   else if anima1 = "pig" then "oink"
              ^^^^^^
Error: Unbound value anima1
Hint: Did you mean animal?
```

The same issue appears when programs evolve. Suppose we write two Python functions:

```python
def sound(animal):
    if animal == 'dog':
        return 'bow'
    elif animal == 'cat':
        return 'meow'
    elif animal == 'caterpillar':
        return '...'

def legs(animal):
    if animal == 'dog' or animal == 'cat':
        return 4
    elif animal == 'caterpillar':
        return 1000
```

Later, we decide to handle humans:

```python
def sound(animal):
    if animal == 'dog':
        return 'bow'
    elif animal == 'cat':
        return 'meow'
    elif animal == 'caterpillar':
        return '...'
    elif animal == 'human':
        return 'huh'
```

If we forget to update `legs`, then `legs('human')` returns `None`. Python gives no warning. The error remains conditional, because it appears only if that function is called with that input.

In OCaml, the natural representation uses a sum type:

```ocaml
type animal = Cat | Dog | Caterpillar

let sound = function
  | Cat -> "meow"
  | Dog -> "bow"
  | Caterpillar -> "..."

let legs = function
  | Cat | Dog -> 4
  | Caterpillar -> 1000
```

If we add `Human` to the type but forget to update `legs`, the compiler points out the omission:

```ocaml
type animal = Cat | Dog | Caterpillar | Human

let legs = function
  | Cat | Dog -> 4
  | Caterpillar -> 1000
```

```text
Warning 8 [partial-match]: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
Human
```

## What Is in a Name?

The scope of a name defines the context in which it is valid. Python's rules for scope are often unintuitive:

```python
day = 'Monday'

def setday(newday):
    day = newday

setday('Tuesday')
print(day)
```

This prints:

```text
Monday
```

The `day` inside `setday` refers to a newly created local variable, not the global `day`. Python implicitly creates variables on first assignment in functions, avoiding an explicit keyword like `let` or `var`. This violates Python's own principle that explicit is better than implicit.

The problem is not limited to global variables:

```python
def end(s):
    last = "x"
    def a(): last = "a"
    def b(): last = "b"
    for c in s:
        if c == "a": a()
        elif c == "b": b()
    return last

print(end("abracadabra"))
```

This prints:

```text
x
```

The assignments inside `a()` and `b()` do not affect the `last` in the scope of `end()`. Python's fix is to use `global` and `nonlocal` declarations. But it is easy for a beginner to forget them, producing conditional runtime errors.

## Python UI Lies

A fundamental rule of user-interface design, including programming-language design, is that things that look the same should behave the same. Consider:

```python
x = 5
y = x
x = x + 1
print(x, y)

x = []
y = x
x.append(0)
print(x, y)
```

The output is:

```text
6 5
[0] [0]
```

Changing `x` also changes `y` in the second case but not in the first. Python provides a consistent-looking interface to value types and reference types, even though they behave fundamentally differently.

The list replication operator makes the same issue worse:

```python
xs = [[0] * 3] * 3
xs[0][0] = 1
print(xs)
```

The result is:

```text
[[1, 0, 0], [1, 0, 0], [1, 0, 0]]
```

The rows are not independent lists. The expression creates repeated references to the same inner list.

Implicit duplicate references can also appear during iteration:

```python
xs = [1, 2, 3, 4]
for x in xs:
    if x % 2 == 1:
        xs.remove(x)
print(xs)
```

This seems to work:

```text
[2, 4]
```

But a small change exposes the problem:

```python
xs = [1, 2, 3, 4]
for x in xs:
    xs.remove(x)
print(xs)
```

The result is still:

```text
[2, 4]
```

The iterator keeps an implicit reference to the list, while the loop body mutates it through `xs`. The language allows the conflict.

## Python Non-Functionality

Higher-order programming, the ability to manipulate functions as values, is important because it enables code reuse beyond first-order abstractions. Python has adopted many higher-order features from the ML family, but its inability to distinguish value and reference types weakens them.

Consider:

```python
def dup(x):
    return (x, x)

def applyfst(f, pair):
    (x, y) = pair
    return (f(x), y)
```

With integers, this behaves as expected:

```python
print(applyfst(lambda x: x + 1, dup(0)))
```

```text
(1, 0)
```

With lists, it does not:

```python
def append0(xs):
    xs.append(0)
    return xs

print(applyfst(append0, dup([])))
```

```text
([0], [0])
```

The definitions of `dup` and `applyfst` are logical, but their behavior changes depending on whether they are used with values or references. In a real program, such functions may come from a library. A user should not need to know the implementation details of a library to use it safely.

The OCaml equivalent has no such surprise:

```ocaml
let dup x = (x, x)

let applyfst f (x, y) = (f x, y)

let inc x = x + 1

let () = assert (
  applyfst inc (dup 0) = (1, 0)
)

let append0 xs = xs @ [0]

let () = assert (
  applyfst append0 (dup []) = ([0], [])
)
```

Even Python's built-in higher-order functions must be used with care:

```python
def listmap(f, xs): return list(map(f, xs))

print(
    listmap(
        lambda f: f(0),
        [lambda x: x + 1, lambda x: x + 2]
    )
)

print(
    listmap(
        lambda f: f(0),
        [lambda x: x + i for i in range(1, 10)]
    )
)
```

The output is:

```text
[1, 2]
[9, 9, 9, 9, 9, 9, 9, 9, 9]
```

It is possible to teach students to avoid such errors by explaining Python's abstract machine. But the point of a high-level language is to raise the machine's level of conversation toward the human, not to lower the human's level of conversation toward the machine.

In OCaml, the corresponding code works as expected:

```ocaml
let apply fs x = List.map (fun f -> f x) fs

let rec range n m =
  if n = m then [n] else n :: range (n + 1) m

let fs = List.map (fun i -> (+) i) (range 1 9)

let () = assert (apply fs 0 = range 1 9)
```

## OCaml Imperativity

There is at least one place where Python is usually considered more usable: mutation and iteration. The following computes a factorial using a mutable variable:

```python
def factorial(n):
    p = 1
    for i in range(2, n + 1):
        p = p * i
    return p
```

The classic recursive OCaml version mirrors the mathematical definition:

```ocaml
let rec factorial n =
  if n = 0 then 1
  else n * factorial (n - 1)
```

Functional-programming experts would usually write a tail-recursive version for performance:

```ocaml
let factorial n =
  let rec loop acc = function
    | 0 -> acc
    | n -> loop (acc * n) (n - 1)
  in loop 1 n
```

But OCaml is not as strict about functional style as some functional languages. We can mirror the Python implementation:

```ocaml
let factorial n =
  let p = ref 1 in
  for i = 2 to n do
    p := !p * i
  done;
  !p
```

The difference is that we must explicitly state that `p` is mutable by making it a `ref`. The `!` operator retrieves the current contents of the reference. This may look less pretty than Python, but it satisfies the principle that explicit is better than implicit.

## Fixing Python?

Realistically, it would be difficult to convince people to switch from Python to OCaml. If Python is used with learners, I suggest these guidelines:

- For misspellings and type changes, introduce automated tests early. Teach function syntax before control structures. Mandate linters such as `flake8`, unit testing with `pytest`, and type hints from the beginning.
- Avoid global variables and local variables accessed by nested functions as much as possible.
- Treat lists and dictionaries as either immutable, or ensure there is exactly one active reference to them at all times.
- Introduce higher-order programming anyway, because it is important. It may be better to let learners encounter Python-specific problems as they arise rather than turning an introductory course into a course on Python arcana.

## Epilogue

There are many scenarios where Python may be a better choice than OCaml. This article considers only the suitability of a language for introductory programming courses. The switch from C to Python helped a larger number of students get into programming. A switch from Python to a better-designed language could have a similar effect in the future.
