What I Wish My Computer Science Degree Taught Me About Programming

A common topic on internet discussions is the practicability of computer science degrees. Students and industry complain they are not being taught actual job skills, while academics maintain that university should teach you a way of thinking, not any particular skill or technology.

I believe there could be a middle ground. At my university, attempts at imparting practical knowledge existed, but were extremely misguided. There was exactly one course about a specific programming language, but it was Ada, a language with exactly 0 hits on StackOverflow Jobs. A course on functional programming didn’t even teach me functional programming, but only writing proofs for hypothetical functional programs. There was one course called “Software Engineering” which would seem ideal to cover industry topics, but it consisted *entirely of drawing UML diagrams*. You can check out the exam and solution for yourself here: Exam Solution

In this article I will go over some of what I feel like were the skills I now consider most important that were completely missing from my curriculum.

#1: IDEs

IDEs manage your code much more than a simple text editor. An IDE manages the libraries/SDKs you are using, constantly runs static analysis (checking the source code for mistakes), and provides debugger integration. It can also provide other tools such as version control integration and viewers for markdown, images, and other data files. IDEs are often even more extensible through plugins.

Static Analysis: An IDE will constantly run something called static analysis on your code, which will provide you with instant feedback on your code, without having to actually build or run it. For example, it will tell you when it can’t find a library you are importing, or when you misspelled a variable name. In statically-typed languages, it can also reason about the types of your variables, so it will instantly know whether you are calling the correct functions on the correct objects with the correct number of arguments. (This is why I highly recommend using the optional type hints in a dynamically-typed language such as Python, as this unlocks your IDE’s power.) IntelliSense/Picklist is a feature that autocompletes names when you are typing them, allowing you to know even sooner whether you are calling the correct names. IntelliSense also adds a whole new layer of discoverability: Given an object of a certain type, you can just try out a few terms to see if a method with that term exists on the object, which saves you an immense amount of googling, and even makes you aware of previously unknown features.

A picklist in PyCharm

Ctrl+Click/Go-To-Definition: In most IDEs, you can hold the control key and click on a name to go to its definition. For variable names and your own class names this just jumps around in your project and is already extremely helpful. But you can actually do it with libraries functions as well, where you will usually find documentation and, depending on the library, even the implementation. (You can Ctrl+Click your way into the Python standard library, and decompile Java .class files, but in other cases you will just see bare headers.) Jumping around also goes the other way: You can click on a definition and find all references to it (Visual Studio Code even lists them by default). Before IDEs, navigating code was a nightmare, so I always kept code in a single file, so I could at least do a simple text search for names (My master’s thesis is implemented as a single 3,300-line code file).

Git: IDEs also (unintentionally) provide a nice and productive frontend for version control systems such as Git. If you are still using Git from the command-line, you need to stop and switch to one of JetBrains’s editors. Even if I don’t use one of their editors for a project, I will still open the project up in their editor JUST for the excellent Git integration. They help me in reading and understand changesets, separating changesets into commits, resolving merge conflicts, looking at old versions of files, and pulling changes non-destructively. I previously used the GUI tools shipping with git (gitk and git gui) for this, but both of them are UI nightmares with encoding and other frustrating bugs!

Changing part of a line in Android Studio (left) vs gitk (right). Android Studio makes the change much clearer.

Debuggers: Debuggers allow you to set a breakpoint at any code line, allowing you to run a program up to a certain line. When it’s reached, your program will pause and examine all variables and their references at that point, allowing you to check if the state is as expected and to track down bugs. You can then even step through the rest of the program line-by-line. I am still amazed this is even possible to do. Ideally you want to divide your code up well so any function can be tested in isolation, but debuggers can rescue you when that is not the case, or when you need to debug library functions.

Great IDE support is the reason that even a poorly designed language such as Java became popular. Cumbersome getters/setters/constructors for data classes can be automatically generated, and overly long class names get autocompleted. The awesome power of IDEs might also be the reason that non-FizzBuzzer programmers are able to hold down a job.

#2: Web Development

Web development is the bread-and-butter of modern programming. In the 2020 Stack Overflow Developer Survey,

  • 55% identify as back-end devs
  • 55% identify as full-stack devs
  • 37% identify as front-end devs
  • less than 24% identify in all other categories

I would even argue you could count mobile devs (19%) as front-end devs, driving this number even higher. As you can see, most developers need to know web tech, and even company-internal and desktop apps are increasingly using HTTP transport.

When you navigate a web site, there are two computer programs running in parallel. One is running on the server (the back-end code, this is where your data is stored) and one is running on your PC or smartphone (= “the client”, this is the front-end code, and is mostly for display). This is why the web is so easy to use: When you go to a site, you are essentially downloading parts of a program, without having to install or manage it yourself.

By historical accident, JavaScript is the only language supported across browsers, so using JavaScript is mostly your only choice for front-end programming. JavaScript is infamous for being badly designed and created in 10 days, but recent releases have made steady improvements. TypeScript is a better alternative, but since browsers only support JS, TS has to be transcompiled into JS, so you still end up with JS.

On the back-end however you are free to use any programming language and any web framework you like. For a first project I would recommend Python+Flask as these are pretty simple and minimal (and don’t require tons of code generation like some Java frameworks). You don’t even need to install a separate database server, you can just use flask_sqlalchemy+SQLite which allows database-like operations on a single file. (To my university’s credit, at least there was a mandatory course on databases.) Here is a YouTube tutorial on Flask.

A good way to explore a bit more is to use the inspector and network inspector inside your browser (Ctrl+Shift+C for Firefox). You can actually see which data packets are being sent, although I do recommend learning the lower-level stuff separately to gain an understanding of the whole stack. I actually used this technique heavily when reverse-engineering the mundraub.org API for use in my Mundraub app. Alternatively, it’s also relatively easy to develop an app and bring it into the web these days (see the next point).

#3: CI/CD And DevOps

Old-fashioned companies had a clear separation between “dev” and “ops”: Developers would write an application, which would then be handed over to the operations teams (sysadmins), which then install/deploy it. The problem here is that this is a very manual process with a lot of ticket-writing and back-and-forth, and that the sysadmins typically tend to be less qualified and don’t know the app as well as the developers. Thus allowing developers to define the app’s infrastructure and building it up in an automated fashion can increase efficiency and reduce time-to-deployment drastically.

CI And Build Pipelines: Modern software teams set up a build server (either cloud or self-hosted), which is responsible for automatically creating a build of the software from scratch, performing static analysis on it (checking the source code), and running the project’s tests against it. This allows devs to see the health of any commit (Does it build? Does it violate our coding style?) and avoids the “But it works on my machine” effect, without being reproducible. For well-tested codebases, CI allows frequent merging of branches which improves team velocity.

Containers: Docker containers go a long way toward the goal of reproducible builds as well. Essentially, a container ships the whole operating system and runtime along with your app, which allows you to tightly control your app’s dependencies. Think of them as heavily optimized virtual machines: They do not ship their own kernel, no unnecessary programs, performance is almost identical to bare-metal, and the layering system means that multiple containers can take up very little space. I still remember the dark days of 2010–2015 when you would try to run some GitHub project, and there would be a 90+% chance it wouldn’t work because you had the wrong compiler, runtime, or libraries installed. (Even today, I still can’t run Friday Night Funkin because my glibc version is mismatched.) Nowadays, you can ensure your app will always work by containerizing it.

CD And Cloud: Cloud computing allows devs to rent servers on the internet, without having to physically handle hardware or networking. I can type a few letters into my terminal and not a minute later, I can access a virtual server anywhere in the world. Even better: As the final step in your app’s build pipeline, you can automatically create the necessary servers for said app, and then deploy the app onto them. For a full end-to-end experience, you can even define your server and network layout in text files (“infrastructure-as-code”) and set your app to scale under heavy traffic (= rent and connect more servers). This enables automation, reproducibility and prevents errors that occur when manually administering a system.

Closing Words

If anyone has good course recommendations for learning more about the three topics, please comment them for future readers.

23-year-old programmer from Germany!