Advice I often give to new programmers
In a recent article, I spoke about why teaching junior developers is often more difficult than teaching advanced skills.
There are many reasons for this, but a lot comes down to habits and priorities, and missing or unpractised mental disciples and metacognition skills.
This article addresses some of the most common challenges I've seen. It contains a=advice that I have found myself giving to junior devs again and again.
If you are early in your learning journey and reading this, try to internalise this stuff. Bookmark this article, revisit it. Talk about it with your peers. Try to grok this stuff, and try to figure out when it does not apply.
Without further delay, here is the advice:
Treat the next developer who looks at your code as your client. This might be you!
Just because the code runs, it doesn't mean it's good. There is a craft to coding well.
You'll hear a lot about all sorts of things while learning to write well. You'll hear about DRY code, loose coupling, locality of behaviour, cohesion and a lot of other things. There's a lot of pretty crazy terminology and wrapping your head around all of it can be overwhelming for someone new.
But there is a shortcut.
There is a lens you can use to better understand a lot of that stuff, and that you can use to write code that is more likely to be good by default.
If you focus on making your code clear and easy to navigate for humans, and take that aspect of the craft VERY seriously, then a lot of good things will happen by default.
A nice way to think about this is: Always consider the next person who will look at your code as your client. You should always do your best that person up for success and happiness.
Good code is clear and it makes its intentions clear. Maintain clarity by being intentional about how you name things, and by keeping things as simple as possible (and no simpler).
Good code doesn't send people on treasure hunts and wild goose chases. Ideally folks should understand a piece of code without needing to navigate between a hundred different files, or by excessive scrolling.
The next person to view your code might be your future self. Still, treat this future-you well. On large projects you will be forced to revisit code you have written before. Pay attention to where your struggles are.
If you write fancy code, that doesn't make you a fancy person
I've seen a lot of new developers try to show off how smart they are by writing overly complicated code and using the fanciest names possible. This is not a good move.
Your goal when writing code should be to solve problems as simply, clearly and elegantly as possible. And to set the next coder up for success.
If your goal is to show people how clever you are at the expense of clarity and simplicity, if you indulge in ego-driven development, you might succeed at impressing some noobs, but nobody who knows what's up.
And you are likely to cause problems for yourself and your team mates.
Overly complicated, hard to read code, that is longer than it needs to be is more likely to have bugs. It's also harder to maintain and upgrade. So the next coder who works on it is also more likely to introduce bugs.
Masters value simplicity.
Take a moment to think about a martial art, for example BJJ, Kung Fu or Karate. Beginners often focus on surface level details, they flail and waste energy and give opportunities to their opponents.
Masters waste no movement. They move with precision and intention. They don't flail around, they are efficient.
If you use fancy words, that doesn't make you a fancy person
Related to the last point - I've seen a lot of folks who are early in their careers try to sound formal or intelligent by using the biggest, fanciest, most convoluted phrasing they can.
This is also worth avoiding.
Here's an example:
I was recently working on PyConZA's opportunity grant process, I sent out an email to an applicant telling them that we didn't have the funds to support them, and that I'd let them know if anything changed.
Their response:
Thank you for your response. I do hope for the very best in the acquisition of more sponsors that’ll therefore facilitate my attendance to this prestigious event PyConZA 2024. Kindly keep me posted if anything arises.
Here is an article about this kind of thing.
Do not guess!
If you are writing code and you are making use of something and you are not super sure if you are using it correctly: Spend time understanding
If you aren't 100% sure how to install a package you need: Read the docs, don't just pip install {your guess}
and hope for the best.
If you have a task to do and you aren't 100% sure of what you need to do: Ask questions. Even if you are very sure of what you need to do, it's worth validating your assumptions.
A lot of junior programmers litter their code with guesses in a way that causes problems in the long and short term. The code they are confident in gets all mixed up with their guess work and so it all the code loses value.
It makes the code more likely to break, and it makes the intention of the code less clear - if you misuse a tool then the next developer who looks at the code can get the wrong idea about what you were trying to do.
It's not possible to have perfect knowledge of what you are doing at all points in time, especially in the beginning. Sometimes you will need to put things in your code that are theories. But it's important to be intentional about it.
If you can avoid guesses completely, then do that!
If you need to make a guess then write some some code that will fail loudly if your guess is incorrect. This could be a test or some assertions. But be aware of your guess and make a point of checking if you were wrong and remembering the outcome.
You can also leave different markers in your code so that you remember to go back and validate things later on. Comments are alright (but they can be forgotten). You can also explicitly raise exceptions or throw errors where your guess is so that the code literally wont run until you have revisited it.
A lot of the danger of guessing will be nullified if you have good tests. But as a beginner, your tests might not be as good as you think. And you might be tempted to guess in your tests.
So it's important to be self-aware enough to know when you are guessing and when you are sure. And self-disciplined enough to make yourself sure.
I want to reiterate a point from this section that is quite important: If your trustworthy code and your guesswork is mixed up and combined, then all your code loses trust.
Don't leave rubbish lying around
A lot of junior developers leave weird junk in their code. Functions that aren't called, branches of logic that aren't used, large blocks of commented-out code. Lines of code that literally add no value and should not be accessed ever.
If you leave rubbish lying around then it's going to slow people down in some way. It can cause confusion, it can cause people to take incorrect actions. Or someone else might be forced to clean up after you.
Keep things tidy.
Here's an example of something I experienced recently on an open source project:
There was a developer working on a feature. In doing so they wrote some really buggy code, they created an abstraction - some reusable functionality - in a way that would cause a lot of bugs. When I pointed out the problem with their abstraction they quickly found a better way to implement their feature and everything worked great.
But they left the dodgy abstraction in place. If anyone else saw it and thought it was good code and used it, then there would be guaranteed bugs.
Errors are great! Learn to use them to your advantage
I lot of new programmers seem to think that errors and exceptions are bad. If an error happens then many simply catch it, write it down, and move on with their code.
Here are some examples:
# Python
try:
stuff
except Exception as e:
print(e)
// JavaScript
try {
stuff
} catch (e) {
console.log(e)
}
Errors, Exceptions and equivalents have been implemented in Python, JavaScript, C++, Java, Rust, Clojure... the list goes on and on. These are all very serious languages that were created very intentionally by very experienced and clever people.
If the correct thing to do with all errors was catch them and write them down, then surely the language designers would have thought of that and implemented some sensible default behaviour?
Errors exist for a reason.
Their default behaviour is already very sensible.
Try to think about why they work the way they do.
Errors and Exceptions can tell a developer that something is broken. This is good because if you know a thing is broken, you can go fix it. Errors are also great because they tell you exactly how a thing broke, what line of code is failing, and what caused that line of code to be executed.
This information is golden.
Code should not fail silently. If the code is broken, it should shout at you and give you as much information as possible so you can fix it properly.
Errors and end users
Of course, if you have some kind of user interface, then having it fail loudly and informatively might not always be the best option. There are times when you will need to hide error details from certain humans.
But think hard about hiding details. Make sure that you don't hide important information from yourself or your team-mates.
Errors and flow control
The other thing that errors are useful for is flow control. In that case you want your code to deal with the error in a useful way.
Here is a very simple example of exponential backoff in Python:
for n in range(3):
try:
page = api_client.fetch_page(*args)
return page
except RateLimitExceeded:
time.sleep((n+1)**2 * 5)
Comments should add clarity, not noise
Comments should be useful to the next person who reads your code. The next person who reads your code will be someone who knows how to read code.
Consider the following:
bubbles = 5 # initialise bubbles so it has the value 5
In what universe would that comment be useful? If you rewrite your code as comments then you are writing the code twice.
Nobody will read that stuff. Because nobody will find it useful.
Nobody will maintain that stuff.
And then if you do write a useful comment somewhere then nobody will notice it's existence because it will be lost in the noise.
Focus on writing comments that would be useful to the humans who will be reading your code.
Commands can be used to:
- explain WHY you are doing something
- explain what a complicated piece of code is trying to achieve
- provide links to documentation that will add clarity or context
Don't just write comments because someone told you that comments are good. Write comments because they make the code better.
Where this habit comes from - cargo-culting
I suspect that people fall into the bad habit of rewriting their code as comments because they have seen that kind of thing in an educational context: If you are learning from a tutorial or book, then the author might have commented every line in order to explain what every line does.
In this case, adding a lot of comments makes sense because the goal is to teach and explain.
A lot of people see things in tutorials and then mimic those things in inappropriate ways. They just assume "this is the right way to do things always".
Always think about how things should be applied in a serious code base. Don't just copy people without thinking things through.
If you haven't heard of cargo-culting in the context of software development, it would be a good idea to look it up.
Do not code with a hammer
Sometimes, when the code just isn't doing what is required, developers try to force it to work.
Sometimes people will make guesses and change lines of code almost at random until things seem to be working. This shows a lack of understanding of foundational skills.
Foundational skills are super important. In an earlier article, I wrote about how missing foundational skills can cause developers to hit glass ceilings in their career growth.
Sometimes the instinct will be to take a broken solution and layer on more complexity until the overly-complicated solution sortof does what it is needed, but with a lot of edge cases and flailing.
If you find yourself being very forceful with the code, if you are coding out of frustration, if you are hitting the code with a hammer to make it work, stop and think.
Generally there is one of two problems:
You might be misunderstanding some foundational mechanisms of how the code works. This is more common than you think. I've seen self-proclaimed "advanced" programmers who were not comfortable with for loops. Check your foundations!
The other problem is often due to starting off with a misguided and overly complicated algorithm. If your code just is not working and you think it should, or you have way too many strange edge cases, it often makes sense to rethink the algorithm from the ground up.
Note, I'm not advocating for rethinking and rewriting large code-bases, but rather being willing to rethink smaller parts that are not working.
A lot of the time, folks try to fix problems by adding more stuff. Often, the solution lies in simplifying.
Take care in naming things. Immediately
By now, you should know that naming things intentionally and clearly is important.
Many junior developers internalise this knowledge, and then develop the bad habit of writing code with terrible variable names, like a
, b
, vh
etc, with the intention of cleaning the names up later.
They know that it's important to choose good names and they tell themselves: It's faster to write the code this way, once it works I'll come back and clean it up.
There are many problems with this.
Let's talk about the false sense of efficiency first:
- Auto-complete is a thing. Writing long names doesn't take much time after the first time because modern code editing tools help you out.
- If the code you are working on has clear names, it will be faster for you to reason about it.
And on the "I'll clean it up later" front:
- You might run out of time and either skip this step, or miss a few things as you hurry through the task
- cleaning it up takes time. If you were planning to save time this is the opposite
- extra and unnecessary edits to the code can have unexpected effects. You could introduce bugs.
There are other downsides too. Imagine you are working on a piece of code that is large, or task that takes time. The problems from above would be amplified. You would need to remember more silly names in more contexts, you would need to revisit more code and risk missing more things and introducing more bugs.
On top of that, if you needed to show your work-in-progress code to someone - a team-mate, mentor or boss - then that would go badly. It would be hard for people to give you input if your code was unclear by default.
So, it's a very bad habit.
People justify and rationalise this bad habit in a lot of different ways. But it seems to me that the real reason folks do this is because developing good habits is hard.
But good habits pay dividends.
More generally, if you add more steps to your process, and set your fallible, human self up so that you need to remember to do more things, then that is very unproductive. It's flailing.
Remember that martial artist metaphor from earlier? How could you achieve high quality code with less wasted movement? Fewer chances for bugs to get through your defences?
Call it what it is
This is another one on naming. Naming is super important, and people sometimes name things quite strangely.
Call a nail a nail. Don't call a nail a hammer.
Name a thing according to what it is or what it does, not what you intend to do to it.
Here is an example that should seem pretty dead obvious:
Let's say you and I are making some potatoes for dinner, and we need to cut the potatoes into pieces with a knife. You would like me to pass you a potato so you can start cutting it up. If you asked me to pass you the "knife" then you would not get what you wanted. If you asked me to pass you the "cut" then I would wonder at your state of mind. If you asked me for a potato then I would give you the thing you asked for.
This seems silly and obvious, right?
But I see this kind of thing in code all the time:
knife = Knife()
for cut in potatoes:
knife.cut(cut)
cut = Knife()
for knife in potatoes:
cut.cut(knife)
Just call things what they are. Communicate like you are talking to a human:
knife = Knife()
for potato in potatoes:
knife.cut(potato)
The End (sortof)
There is a lot more advice that I could give - there is a lot more to be said about writing good code, and good tests. A lot to be said about how to function in a team. A lot to be said about how to pursue growth.
So this isn't really the end, but it's the end for now.
I'd like to close off with the Zen of Python. Even if you are not a Python dev, it's worth reading and thinking about deeply.
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
Want to learn from me?
I'm running some technical workshops and long-term mentorship over at Prelude. These are damn fine learning experiences for individuals and teams.
The training covers skills such as: Python, Django, HTMX, AlpineJS, Git, Tailwind, Playwright and more.