ITMGT 25.03, Lecture 1

2025-07-03

This article is my attempt to preserve the way I approach introductory programming before my mind drifts too far from beginners' experience for my input to be useful to them.

It's appropriate that programming languages are called "languages." They are a means of communicating ideas, except instead of communicating with other people, programmers communicate with computers. How do you start learning your first programming language? I think the best way is to learn a little theory, some grammar and vocabulary, and then immediately start solving exercises. There's no quicker -- or, in my opinion, other -- way to really become a programmer. This article is about the little theory that you'll need to know before diving in.

Computers

To manage our personal and social affairs, we humans need to keep track of information.

Early in history, we would have done such tracking with physical tools and physical marks. If an ancient farmer needed to track the number of cows he is selling on the market, he could have made marks that he understood to mean certain numbers on a clay tablet, papyrus, paper, or some other material, even if such markings were as simple as a stick tally. Such physical markings are perfectly adequate for many use cases even today, where some people (including myself) like to keep physical pens and physical notebooks.

Of course, as the scale of our problems increases, the scale of the information involved also increases. Without changing the fundamental concept of making marks to remember information, how do we deal with the increasing scale of information? We can increase the speed at which we make marks, we can socially agree on marks and combinations of marks that hold deeper meaning and therefore increase the information density of marks, and we can also decrease the size of the marks themselves, which can make the marks easier to transmit and store. That is why a book, printed with a press and written in English, is more powerful as an information-keeping and information-transferring instrument than a clay tablet, created with a stick and written in scribbles that no one knows how to interpret.

The invention that really unlocked a new dimension to information storage and transfer was the vacuum tube. A vacuum tube is composed of three parts: a source of electrons, a destination for the electrons, and a control grid placed between the source and the destination that can either allow or block electron flow between the source and the destination. If you apply a small negative charge to the control grid, you can significantly reduce or even completely stop the flow of electrons between the source and the destination. Depending on whether or not you have a small negative charge flowing to the control grid, your vacuum tube is either "on" (i.e., electrons are flowing) or "off" (i.e., electrons are not flowing). By sending a small amount of electricity -- something that is relatively very easy to transmit compared to physical objects -- you can change the state of a vacuum tube and therefore transmit information.

One vacuum tube turning off and on might not be that useful on its own. However, multiple vacuum tubes, each either "off" or "on", could be used to represent more complex types of information using the binary system. Most of us count numbers using the decimal system, where we have ten digits available to us: 0123456789. When we run out of digits, we add another digit to the left of the number and roll the existing digits back to zero. The binary system is similar, but it only has two digits: 01. The representations of zero and one in binary and decimal are the same: 0 means zero and 1 means one. However, binary runs out of digits quickly, so to represent three, binary needs to add a place and roll back the existing digits to zero. Decimal represents three as 3, but binary represents three as 10. While more digits are necessary, we can see that binary digits, and therefore vacuum tubes, are perfectly capable of representing numbers, and therefore almost any type of information. Additionally, vacuum tubes could be arranged into "logic gates." With vacuum tubes and some supporting circuitry, it is possible to invert and combine signals. This means that vacuum tubes can also perform computations in addition to storing information.

A large enough vacuum tube network could be used to store and compute arbitrary amounts of information using only electricity, which, again, is easier to transmit than physical material. Of course, early on, vacuum tubes were too large and cumbersome to actually build a massive information network with. However, the silicon transistor, which was invented later and is many times smaller than a vacuum tube, can do the same things a vacuum tube can do. The first computers rapidly grew smaller and more powerful using better and better transistor technology. The computers now still use transistors. Modern transistors are incredibly small, but the principles of binary and logic gates behind them remain the same.

Abstraction

Users of computers almost never think about transistors. All they know is that when they press buttons, computers calculate things or perform actions. This is how it should be.

This is the principle of abstraction. Users of a machine should only have to care about how they interact with the machine and not about the way the machine works. Drivers of cars should not have to think about the engine, only about how, when they press the gas pedal, the car moves forward, and how when they press the brake, the car slows down. Users of computer programs should not have to think about the logic in the codebase of their web browser, only about how when they open their web browser, they can search for useful or interesting websites.

Abstraction happens in layers. Users might not have to think about code, but programmers of applications (i.e., programs that normal users use) do. But programmers of applications should not have to think about how electricity flows between transistors to add two numbers, only about how, when they enter 1 + 1, the result is 2. Funnily enough, two of the keys of being effective as an applications programmer are, first, in knowing how computers work and, second, in being able to forget about how they work.

Data and values

As an applications programmer, you will mostly work in the abstraction layer that cares about data. Data are the raw values that can be interpreted to reveal information or knowledge.

There are four types of raw values that will be relevant to you as a Python programmer.

Notice that all of these values ultimately derive from binary representations of whole numbers. That is, after all, the only way that computers can store information. We simply use mappings between whole numbers and other systems of information (e.g., Unicode) to translate numbers into forms that are more useful to humans.

Literals

The easiest way to include values in your code is to use symbols which invariably represent values. The symbol 1 in Python will always mean the integer 1. The symbol 3.5 will always mean the float 3.5. True will always mean the Boolean value for true. These symbols are called literals.

Expressions

You can perform computations on values in Python.

Arithmetic

Arithmetic operations use integers and floats to produce integers and floats.

Comparison

Anything that can be compared can be subjected to a comparison operation to produce a boolean.

Logic

Booleans can be subjected to logic operations to produce other booleans.

Simplification

Note that these operations will ultimately simplify or evaluate to another value. For example, 11 + 22, as an expression, will simplify to 33. This fact may seem trivial, but understanding what pieces of code will simplify to a value (and knowing what type the simplified value is) will be critical in fluently writing code later in your programming journey. We will return again and again to the concept of expressions and simplification over the course of these lessons.

Binding values to names

Literal, invariant values alone are not useful other than to calculate things. To perform logic, we will need to be able to bind values to names. These are called variables, and in Python, a variable is declared as such to bind the name on the left to the value on the right:

x = 11 + 22

In code that follows, the expression x will simplify to the value 33. The transistors in your computer's ephemeral memory will remember that, in the context of the currently-running Python program, the symbol x is bound to the value 33. Once your program terminates, the computer will forget everything that the program stored in ephemeral memory.

x can be re-bound to another value later in the code if needed.

Functions

Values, themselves, are not the only things in Python you can give names. If you want to run well-defined computations on different input values, instead of repeating the code in each part of the program where you need to compute the result, you can give a name to the computation. These named blocks of code are called functions.

Minimally, a function in Python only needs to have a name and a block of code. By default, variables declared inside the function's block of code are scoped only to that function; they cannot be accessed from outside.

def some_function():
  x = 33
  print(x) # The print function displays the result of the given value on the terminal

The only thing that the code above does is define the function. It does not actually run the function. To run the block of code inside the function, you may call or invoke the function as such:

some_function() # Note the parentheses, that is how you invoke a function.

Of course, having a function that merely assigns x = 33 inside itself and prints the output to the terminal is not very useful as a calculator. That is why Python allows you to give input to your functions and also retrieve output from your functions.

To give input to your functions, make your function take parameters.

def greet(name):
  print("Hello, " + name)

You may invoke the function as such by giving the invokation a specific value to bind to name:

greet("Gustave")

When this specific invokation of greet runs, the specific string "Gustave" will be bound to the name parameter. This invokation should print the text Hello, Gustave to the terminal when run. Strictly speaking, name is a parameter and "Gustave" is an argument. I am not too pedantic when it comes to parameters and arguments, but other material might treat them differently.

If needed, you can give a function multiple parameters.

def print_product(x, y):
  print("The product of " + x + " and " + y + " is: " + (x * y))

Note though that the actual function invokation will not, in itself, simplify to a value unless you instruct the function to return a value.

def multiply(x, y):
  return x * y

The multiply function above returns a value, so an invokation of multiply should be expected to simplify to something.

# Assuming you defined multiply
my_product = multiply(11, 3) # The name `my_product` should now be bound to the value 33
print(my_product)

If you run the code above, the terminal should display the number 33.

Functions have an obvious use case as a way to store a complex calculation instead of repeating the code at every site that the calculation needs to occur at. For a slightly more complex example:

def fahrenheit_to_celsius(fahrenheit_temp):
  return (5 / 9) * (fahrenheit_temp - 32)