I’ve been teaching programming (mostly Python) for more than 30 years. But in about a year of coding with AI, I haven’t defined a single data structure or database schema — and I’m accomplishing more, in less time, with fewer bugs, than I ever thought possible. Agentic coding is revolutionary, but it’s part of a larger story that has played out over more than 70 years. In this essay, I look at the evolution of programming languages as successive waves of higher levels of abstraction. I consider what that meant for teaching and learning coding, and how that should influence what we teach as AI becomes the dominant form of programming.
When computers were first invented, there was no such thing as “programming.” Each computer was a special-purpose machine, engineered to solve specific problems. If the problem changed, or if the computer didn’t solve it correctly, you had to make physical changes to the computer. “Debugging” meant cutting wires and soldering parts.
Then some smart people realized that we could separate the computer (the “hardware”) from the instructions and data used to solve the problem (the “software”). No longer did you have to think in terms of wires and logic gates; you could think in terms of instructions and information — verbs and nouns, represented as high and low electrical voltages, 1s and 0s, which we know today as “binary digits,” or “bits.”
Bits were the programmer’s alphabet. Just as we can use letters to write both verbs and nouns in English, programmers could use bits to give the computer instructions and then to also provide the values that the instructions acted on. But programming using bits was hard to learn, not to mention tedious and error-prone.
The power of abstraction
Early programmers quickly adopted the principle of abstraction, combining smaller pieces and processes under a single concept. Abstraction allows us to think and communicate at a higher level. And because our minds can only handle so many things at once, abstraction also allows us to solve larger problems.
Imagine that you’re a traffic planner, trying to understand the ramifications of replacing a traffic light with a traffic circle. You’ll think about the problem in terms of cars. But “car” is an abstraction: If you had to consider all of the parts and processes from each of the vehicles in the intersection, you would never be able to think clearly about the traffic patterns. Communicating your thoughts about those changes would be nearly impossible, because you would be bogged down by low-level details.
The abstraction that programmers adopted became known as assembly language, instructions they could use to control a computer. Programming in assembly was still hard, but it was easier than working with raw binary digits. Moreover, these commands had semantic power, allowing you to think in terms of mathematical operations (e.g., addition and multiplication) and operations within the computer (e.g., moving a value from the CPU to memory). Chip manufacturers competed on the basis of not only speed, but also what high-level instructions their chips supported.
With higher-level programming abstractions, we also got new techniques:
- Repeated lines of code could be replaced with loops, iterating numerous times.
- Code that appeared in multiple locations could be put into subroutines.
- CPUs offered “registers,” a kind of on-chip variable, allowing you to store a value and then refer to it later.
- We saw the beginning of higher-level data structures — integers and floating-point numbers, and even use of integers to represent characters, rather than numbers.
Higher-level languages, higher-level techniques
Why stop with one layer of abstraction, when you could have two? Higher-level languages, such as Fortran, Lisp, PL/I, and (eventually) C, gave us even more semantic power. They looked something like English, requiring translation (“compilation”) into machine code before they could execute. (Fine, Lisp doesn’t exactly look like English.) But the real power of these languages was that both the instructions and the data were using higher-level abstractions than assembly language provided:
- You could define functions, allowing you to add new verbs to the system. Moreover, these functions had human-readable names, giving them semantic power. A function could also be defined on the basis of other functions, giving you an almost limitless number of abstraction layers, each hiding the details of its lower-level parts.
- You could use a wide variety of data structures, many of which hid the underlying bits in which things were stored. You could calculate with integers and floats and work with text (“strings”). You could even combine several such values into an array (all of the same type) or a struct (of mixed types). So you could think in terms of an employee’s salary history, a book’s publication information, or someone’s investment portfolio.
These functions and data structures were ultimately stored as bits in the computer’s memory. But these higher-level functions and data structures allowed you to hide those underlying details, making it possible to tackle increasingly larger problems. Moreover, these higher-level languages lowered the bar to programming even further, allowing a larger number of people to try their hand at solving problems using their computers. However, some of them (e.g., C) still required that we manage the computer’s memory manually.
With these higher-level languages, we started to develop new techniques, including functional and object-oriented programming, which made it easier to create higher-level abstractions for both instructions and data. New languages, such as Smalltalk in the 1970s and C++ in the 1980s, made it possible to use these techniques in our own code.
Python (and friends) arrive on the scene
Starting in the late 1980s and early 1990s, we got a bunch of even higher-level languages, including Tcl, Perl, Python, and Ruby. These languages were originally seen not as full-fledged languages, but rather as providing the “glue” that allowed you to connect different parts of a computer system, as well as automate processes more easily. They were thus dismissed by many serious programmers, called “scripting languages” to emphasize their non-seriousness.
But it turns out that these languages were anything but toys. Yes, their syntax was typically easier to understand than others, and it was easier to jump in and start coding because of their dynamic typing and automatic memory management. But their main advantages were the high-level abstractions they provided:
- Functions: These languages came with a huge number of convenient, expressive functions and methods. And they made it easy to define your own functions, too, with a variety of argument and parameter types.
- Data structures: Even more important, these came with a wide variety of high-level data structures, including powerful and easy-to-use strings, lists (arrays), dicts (hash tables), datetimes, and many more. They typically came with a library of additional data structures, too. And you could define your own data structures, typically using object-oriented programming techniques, with a minimum of syntax and difficulty.
- Third-party add-ons: These languages all arrived at the beginning of the “open source” software movement, which emphasized the benefits of sharing code with others on the Internet. Each language quickly developed its own archive (e.g., Perl’s CPAN and Python’s PyPI) for third-party code contributions. This meant that if you’re trying to solve a problem, and others have solved these problems before, you’ll likely find a solution (or part of a solution) on that third-party archive. These include everything from colorized text (Rich) to data analysis (Pandas and Polars).
These higher-level abstractions — along with supportive communities and easy-to-handle syntax — introduced many newcomers to the world of programming. And they inspired new languages, such as JavaScript, which includes many of the same ideas.
What to teach, what to learn
When I started my CS education in the fall of 1988, most universities taught their intro course in C. To some degree, they did so because it was the most popular language in industry. But they also taught it because C provides a relatively thin layer over assembly code. There was a strong feeling among faculty that to learn programming, you need to start from the lowest-level basics, working your way up to higher-level, more abstract programming languages. They argued that you can only really understand object-oriented and functional programming if you have a solid base of low-level C coding.
But MIT had a very different perspective: They saw programming as a combination of mathematics, philosophy, and linguistics, and taught it that way. Our intro class, known internally as 6.001 and to the rest of the world as Structure and Interpretation of Computer Programs, used Scheme, a version of the Lisp language, which was (unusually) both a very high-level language and a very old one. 6.001 focused on high-level data structures, functions, and how a programming language really works behind the scenes. We learned about variable scoping and bindings, functions as first-class objects, and object-oriented programming.
Never did we think in terms of bits and bytes. Or assembly language. Or memory management. Or what chip we were even using. The focus was on the high-level principles of programming, remaining at that high level of abstraction.
They weren’t trying to say that low-level knowledge is bad or useless. (Another course, 6.004, gave us the chance to learn that topic.) They were, however, trying to say that in the modern world of programming, it’s more important to understand high-level abstractions — how they work, and how to combine them — rather than the underlying machine code.
Nearly 20 years ago, MIT decided to switch away from Scheme for their intro class, to use a language that reflected their high-level approach but was also popular in industry. The language? Python. Thousands of MIT students have thus learned to program with a high-level, dynamic language with rich data structures, first-class functions, support for object-oriented programming, and a warm community of fellow users. Universities around the world have followed MIT’s lead, contributing in no small part to Python’s immense popularity.
How (and what) should AI-native coders learn?
A little more than one year ago, I was convinced that AI could help us code, but that we were a long way off from AI writing code for us. I was wrong; I’ve been using Claude Code and other AI coding systems since January, often for 6-8 hours each day, and I continue to be floored by how much I can get done. I’ve created oodles of functionality for LernerPython, some open-source projects, and a number of things that might turn into online businesses.
For now, I believe that I’m only able to get so much done with Claude Code because of my many decades of programming experience. But my experience here has to do with engineering systems — understanding what the different parts of my program need to do, how to separate them, how to put in guardrails so that I can be sure the software works now and in the future, among other things. I haven’t once modeled a data structure, created a new class, defined a database schema, or implemented a login system, although I have approved specs authored (and then implemented) by Claude Code.
This feels weird. And satisfying. And raises numerous questions about what we should be teaching the next generation of programmers.
I’m convinced that we’re seeing the birth of a new, higher level of abstraction. Just as Python developers don’t need to worry about what instructions are supported by their computer’s chipset, AI-native programmers — people who learn to code through AI, rather than adding AI to skills they already have — don’t need to learn about integers vs. floats, strings vs. bytestrings, or even lists vs. dictionaries, at least not at the beginning. What we long regarded as high-level data structures are now below the abstraction barrier, irrelevant to many coders until they decide to peek behind the curtain.
For now, I still believe that learning to code the old-fashioned way, getting exposed to data structures, functions, and other techniques, is useful. But within a few years, you’ll be able to have a successful coding career without knowing how to implement a dictionary or what “hashable” even means.
The best universities will teach these topics, and the best coders will learn them, because understanding the underlying layers, breaking through the abstraction barriers, can inform and influence your thinking. But just as you can get a coding job in 2026 without knowing the ins and outs of how an operating system works, or how much data your CPU can retrieve in a single clock cycle, you’ll be able to get a coding job in 2036 without knowing the ins and outs of how a programming language works.
Am I arguing against my own courses and products? Not at all: My courses are for people who want deep understanding, exercises, and a chance to ask questions during live office hours. And I still think that knowing Python and Pandas is a great asset in today’s world. I’m adding office hours and courses about AI, which I see as complementary to the existing content, rather than replacing it.
What AI-native coders will need to learn
I increasingly think that people learning programming will need to learn more system-level thinking. They won’t need to know how to define a database, or the different kinds of indexes we might use, but they will need to know that databases exist, and that different types (relational vs. document) can affect performance and data integrity. They’ll need to know that programs can read from a variety of Internet APIs — and how to do so safely and securely. They’ll need to know how to provide an API for others to read from, and to consider the security implications of doing so. They’ll need to think about designing UIs using a combination of textual specifications, screenshots and sketches — followed by many iterations.
They’ll need to think about different document formats. About sending and receiving e-mail. About accessibility. About internationalization and localization. About what happens in the browser vs. on the server. About what languages we want to use, and why.
Most importantly, they’ll need to learn how to ensure that AI is writing solid, reliable code, both now and as it gets modified, extended, and debugged over time. This means teaching them, from the get-go, about automated testing, code coverage, mutation testing. Not to mention backups, frequent commits to Git, and extensive logs. And also security — making sure that any API, site, or app they create is as secure as possible from bad actors.
Compilers are deterministic, consistently turning the same source code into the same executable. Many programmers argue that AI is problematic because it’s fundamentally non-deterministic, and will produce different solutions each time it gets the same specification and instructions. I would counter that people, who currently write the majority of the world’s code, are also non-deterministic — and yet we’ve somehow managed to create incredible software over the years. But it’s true that we shouldn’t just trust AI to do the right thing all of the time.
I’m starting to put together a completely new “AI coding for non-programmers” course, which I’ll offer both on LernerPython.com and to companies around the world. (I’ll announce it to my Better Developers list when it’s ready.) I’m already playing with the ideas of what verbs and nouns an AI coder will need to know and understand in order to be effective. It won’t focus on Python, but rather on the problems that people and companies have, and how they can fix those problems using coding.
Every time programming has added a layer of abstraction, we’ve become able to solve larger and more complex problems, and more people have been able to join the fold of programmers. I believe that AI coding will do both — but it will radically change what coders need to learn, and how we teach them. I’m excited to take on this challenge, and look forward to discussing it with others in the industry.
