Advice for Learning How to Program

Getting Started

I think the best place to start is Harvard’s CS50. From there, you can download Python and read Automate the Boring Stuff with Python. It’s also crucially important to have a solid understanding of discrete math, which I’ve discussed in my how to learn math page.

However, neither CS50 nor Automate the Boring Stuff cover an essential component of programming: writing well-designed code. I recommend supplementing both of the above with How to Design Programs which uses Racket (which I recommend you download and play around with). HtDP and Racket were the bread and butter of Northeastern University’s introductory computer science course, CS 2500. The main idea that CS 2500 and HtDP impart is that it’s important to organize all of your code into well-documented, well-tested functions, where each function:

  • Accomplishes only one task
  • Has clear documentation explaining what it does
  • Can be used in multiple other places
  • Breaks our code down into easily understandable components
  • Allows us to test individual components of our code by testing the functions

This provides several robust benefits:

  1. Our programs will be much easier to understand, change, and maintain
  2. We don’t waste time writing the same code over and over again
  3. We can very easily break down complex tasks into simple, understandable code by combining small, understandable, and reliable functions
  4. The problem of understanding what our programs do is reduced to the problem of understanding each of the functions
  5. Our code is much easier to test, provides evidence that it’s reliable, and helps us pinpoint the location of bugs

Let me provide an example that you will be able to understand after you’ve programmed for a little bit. Frequently in robotics, data science, machine learning, etc. we’ll have two pieces of data called vectors that are essentially just a list of numbers, and we would like to perform an operation called the dot product on these two vectors. The dot product works like this: given two vectors $v = [v_1, v_2, \ldots, v_n]$ and $u = [u_1, u_2, \ldots, u_n]$, the dot product is the sum $v \cdot u = v_1 \times u_1 + v_2 \times u_2 + \cdots + v_n \times u_n$. Notice that we can’t take the dot product of two vectors if they don’t have the same number of elements!

Compare the following two implementations of the dot product in Python:

def dot(v, u):
    if len(v) != len(u):
        raise ValueError("Vectors have different length!")
    result = 0
    for i in range(len(v)):
        result += v[i] * u[i]
    return result
def dot(v, u):
    return sum(a * b for a, b in zip(v,u))

These two implementations do the same thing, but most seasoned software developers I know would greatly prefer the second. It’s much shorter, much easier to understand, and obviously correct. In the first implementation, we need to make sure we’ve written our conditional correctly, that we’re raising the correct error, and that the indexing we’re doing in our for loop is correct. All of these things are neatly handled in the second implementation by the list comprehension and zip function. By combining a handful of simple, reliable, general-purpose building blocks, we’ve made much better code.

Here’s another big idea: it doesn’t matter if you write something like the first implementation on your first try. Putting the specific implementation details in a function, (dot, in this case) allows us to refactor our implementation without breaking or changing any other code. We can think of functions as constituting a “contract” with anyone who calls our code: they provide the specified inputs, and the function provides the specified output. How a function does this is (more or less) its business: it’s a black box, so if we change what happens inside the black box, no one will notice.

There are many other useful techniques from functional programming that are important to get familiar with: recursion, first-class functions, and common functions like map, filter, and folds..

Next Steps

After you understand the basics of programming, a natural next step is to learn about object-oriented programming, which essentially extends our ideas about functions to larger parts of our program: collections of related functions and data. A natural continuation from Harvard’s CS50 and Northeastern’s CS2500 is Northeastern’s CS 2510. These classes introduce Java, which popularized the object-oriented approach to programming. CS 2510 shows how to write code in Java using the functional techniques introduced in CS 2500; this provides a look at how powerful, robust techniques from functional programming can be translated to Java to solve problems in ways that wouldn’t occur to students who have only written imperatively. However, it’s clear by the end of CS 2510 that many of these techniques are a bit contrived and elaborate, so the latter half of the course introduces basic tools of imperative programming like mutation and loops and students begin writing Java “normally.”

The next course in the sequence, CS 3500, serves as a larger scale course on software design and refactoring, and focuses on writing code with the Model-View-Controller design pattern and then refactoring and expanding it over the course of several assignments. The course draws heavily on Effective Java by Joshua Bloch and Design Patterns: Elements of Reusable Object-Oriented Software (commonly referred to as “gang of four”), both of which are crucial resources. I’d also include Game Programming Patterns as a reference for useful programming patterns, most of which arise in all kinds of software.

After you feel familiar with Java, I highly recommend you move over to C# because it has loads of much-needed features like:

Of course, there is more to programming than object-oriented code. C, C++, and Go are popular languages that don’t require you to use the Java/C# OO paradigm, but C and C++ are showing their age and have a lot of sharp edges. Go is much more modern, but it suffers from a handful of weird design decisions that really hold it back. I highly recommend learning Rust to fill the niche of these languages; it has a steep learning curve, but learning how to write good Rust code will help you write better code in other languages.

Git, the Shell, Debugging, and more

I highly recommend The Missing Semester of Your CS Education to learn about essential tools that computer scientists are expected to learn about through osmosis. Git is a major one, and sites you may have heard about like GitHub and GitLab exist to host Git repositories. Besides the basic Linux terminal commands like cd and ls, I recommend you useful commands like man, find, and grep. Spending a couple minutes to learn the options for each of these commands can save hours of work.


The final skill that I feel is necessary to mention is algorithm design. I’ve neglected talking about performance until now, mostly because performance is much easier to fix retroactively than design. However, performance is still paramount in many settings. I don’t think there are a lot of great resources to self-study, so I’ll halfhearted recommend Algorithms because it’s the standard text, and recommend specifically the chapters on Big-Oh notation, divide-and-conquer, greedy programming, dynamic programming, graph algorithms, and linear programming. You can practice these techniques and do interview problems on LeetCode.

Further Reading

From there, you have the foundations to pursue a lot of other subjects. There’s a wealth of resources and books on graphics programming, game programming, network programming, systems programming, web design, and more. I also recommend checking out MIT OpenCourseWare.

This post is licensed under CC BY 4.0 by the author.