So far we have been talking to the shell, and our programs have been a sequence of commands that the shell has executed one after the other. But there are many other languages we can talk to the computer —assuming, that is, that we are running a program that understands them.
Python is a great language to learn. Invented by Guido van Rossum in the late 1980s, it has become extremely popular. It emphasizes readability, making it simple to write programs that will be easy to read an understand by other programmers (including you, some months after you wrote the program). It is clear, concise, and it runs pretty much everywhere. It is often recommended as the best language to learn programming, but it is also a very powerful language: it runs many web sites, and it is the language of choice for many scientists and engineers.
I won’t be able to give you a full account of Python. My goal is to help you understand what programming in Python looks like, and how it is done in the context of the shell. By the end of this chapter you’ll have written and run a first Python program, and you’ll be on your way to learning much more.
Installing Python
Most systems come with Python pre-installed. You can check if you have it:
which python
If not, install it using your package manager:
# macOS with Homebrew
brew install python
# Ubuntu/Debian
sudo apt install python3
The modern way: uv
Python’s traditional package manager, pip, needs to be installed separately (though some distributions include it). But there’s a newer tool that’s worth learning from the start: uv. It’s faster, simpler, and handles many of Python’s traditional pain points.
Install uv:
# On macOS/Linux:
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or with Homebrew on macOS:
brew install uv
With uv, you can even install Python itself:
uv python install
Running Python
You can start playing with Python simply by running it:
python
You’ll see something like this:
Python 3.12.1 (main, Dec 18 2023, 09:45:12) [Clang 15.0.0] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
Or with uv, which ensures you’re using the right Python version:
uv run python
The beauty of uv is that it manages Python versions and dependencies automatically. No more worrying about virtual environments or conflicting packages.
Now you are talking to Python. In particular, the program you are talking to is called the Python interpreter, because it will interpret python commands as they arrive. You’ll see that the prompt is different than the one you got on the shell: by default, it will be >>>
.
Let’s explore a bit what you can ask Python to do:
232 + 345.0/14.0
256.64285714285717
Functions in Python
Let’s say we want to do something a bit more exciting: how much is 2 to the 34th power? Python has a function to do that. A function is a piece of code that will do something for you. You call it by name, followed by the arguments to the function —the values to which the function will be applied— enclosed by parentheses.
For example, Python has a function called pow
that will compute the power of two numbers for you. You call it with two arguments: the number that you want to raise, and the number to which you want to raise. So if you ask for this,
pow(2, 34)
17179869184
How about a square root? Python also has a function to do that, but it is not loaded by default. In order to use it you first need to ask Python to import its math
library:
import math
To start using the functions coming in the math
library you need to prepend their name with math.
, so that Python will know where to look for them. You use them like this:
math.sqrt(12) / math.cos(2 * math.pi)
3.4641016151377545
Note that we’ve also used math.pi
. This is not a function, but a variable that Python has preloaded for you with the value of $\pi$ with many decimals,
math.pi
3.141592653589793
Variables in Python
You will have noticed that we didn’t have to prepend the math.pi
with a dollar sign, as we had to do when we wanted the value of shell variables. Python is simpler in that way: you just assign a value to a name,
diameter = 23.0
then use the name of the variable wherever you would have used the value it contains,
diameter * math.pi
72.25663103256524
Defining your own functions
Python, like most languages, lets you define functions. This one will compute the longitude of a circle given its diameter:
def longitude_circle(diameter):
return math.pi * diameter
Write it down to the Python interpreter just as it is shown here, respecting the spaces before the second line, and do not forget the colon after the closing parenthesis of the first line.
Python is a bit peculiar with spaces at the beginning of lines: whenever a piece of code belongs to a higher instance, it has to be indented a fixed amount with respect to it. In this case, the line implementing the function belongs to the function definition that started in the first line. There is a colon at the end of the first line, saying “here comes my content”, and then the content gets shifted to the right. This rule —forced indentations of blocks— makes programs very readable.
Let’s call the function, for example, with the diameter of the earth (12742 km, or 7918 miles):
longitude_circle(12742)
40030.173592653588
When Python has seen longitude_circle
, followed by something in parentheses, it has understood that you were calling a function. It has searched among the functions it knows about (including the ones that are always available, like pow
, the ones you might have imported, and the ones you might have defined, like longitude_circle
), and has promptly found a definition for it. It has checked that the number of arguments you are giving (one) matches the number of arguments that the definition of the function is expecting (one, named diameter
), and it has started to run the function.
The first thing it has done is to create a new variable named diameter
, and assign to it the value with which you are calling the function, 12742
. With this it has done the math as you have defined it, and it has returned the result.
Let’s do another example. Say we want to know how long it would take to go around a circle at a given speed. We can first compute the longitude, then divide by the speed:
def time_to_circle(diameter, speed):
length = math.pi * diameter
return length / speed
There’s something ugly about this function. Want to try to figure out what it is?
We’ve actually told Python how to compute the longitude of a circle again. That is a bad idea. In general, it’s much better and safer to use the functions we’ve already defined: this is why we write them in the first place. Check this out:
def time_to_circle(diameter, speed):
return longitude_circle(diameter) / speed
Isn’t it much better, and more readable? Let’s try it. How may hours would it take to circle the earth at 120 km/h?
time_to_circle(12742, 120)
333.58477993877994
Not bad, but how many days is that?
But wait again; I don’t know about you, but I am getting tired of writing the diameter of the earth time and again, mostly because I don’t remember it between one time and the next. Let’s make a variable for it,
earth_diameter = 12742
And now we compute the number of days, simply dividing by 24 the value returned by time_to_circle
:
time_to_circle(earth_diameter, 120) / 24.
13.899365830782497
Exit Python: the end of file, C-d
When you are done interacting with Python you may exit the interpreter with C-d
(press d
while holding control). The C-d
is understood as an end-of-file marker not only by Python, but by all UNIX programs. When you are talking to python
it sees its input channel as something equivalent to an ever growing file: when you type a line and press Enter it is sent to python
as another line in the never ending file, and executed. When you press C-d
it understands that the file-like object is is reading for input has finished, and exits.
Writing a Python program
We can talk to Python one sentence at a time, as we have done until now, or we can write in a file all that we want Python to do and then ask it to do whatever the file says, as if we were writing a note with a list of instructions. Let’s open a file circle.py
,
The .py
is the traditional extension of Python programs. Write your program in it, just as if you were writing directly to Python:
import math
def longitude_circle(diameter):
return math.pi * diameter
def time_to_circle(diameter, speed):
return longitude_circle(diameter) / speed
time_to_circle(1234, 23)
Save and quit when you are done (remember, escape, then :wq
). Can you think how you’d actually run it? Just as with the shell, you’d ask Python to do it:
Nothing. Python did indeed return the result of the time_to_circle
function, but it stayed within Python. And now you are not there, you are on the shell. In order to ask Python to send the results to the outside world you need to print
them. Change the last line so that it says
print(time_to_circle(1234, 23))
and now run again,
168.553275414
Much better.
Command line arguments for your Python program
It is rather sad that the diameter and the speed are hard-coded. If you want to run it with a different diameter you’ll have to open the file, modify it, and save it before you are ready to run the program again. There is, of course, a solution: you can send command line arguments to your Python program.
It’s actually quite simple. You need to import sys
, and then you’ll have access to the sys.argv
variable. This is a special kind of variable named list. It can contain many elements that you access with an index enclosed in brackets: you find the first command-line argument in sys.argv[1]
, the second in sys.argv[2]
, and so forth. Edit the program, and make it look like this:
import math
def longitude_circle(diameter):
return math.pi * diameter
def time_to_circle(diameter, speed):
return longitude_circle(diameter) / speed
import sys
diameter = float(sys.argv[1])
speed = float(sys.argv[2])
print(time_to_circle(diameter, speed))
There is a catch, so let’s follow it closely. First we import math
, because we know we’ll need math.pi
. Next we define the functions. So far it tracks exactly what we were doing when we were talking to Python real-time. Then we import sys
, because we want access to its argv
variable. But when we assign the values of the arguments, in sys.argv[1]
and sys.argv[2]
, to the diameter
and speed
variables, we are calling a previously unknown float
function.
It turns out that Python makes a distinction between strings, which are pieces of text, and numbers. The command-line arguments coming in sys.argv
are strings, and we want to convert them to floating-point numbers. This is what the float
function does.
We are ready now to call the program with arguments:
314.159265359
The shebang
So we’ve written a Python program, but we have to run it with an explicit call to python
. Shouldn’t it be aware of who is the one who has to run it? There is a trick to do just that. Of course, the first thing to do is to make it executable, just as we did when the shell script when we wanted to run it by itself:
Now when we try to run it the shell will open it. If the first line it finds looks like this:
#!/usr/bin/env python3
it will strip the #!
from the beginning, and it will understand that the rest is the interpreter that will be able to run what’s coming next. Using env python3
ensures that the system will find the appropriate Python 3 interpreter in your PATH.
How can you know where the python3
program is? Use which
,
which python3
The env
command will find the appropriate Python interpreter in your PATH. If you add the #!/usr/bin/env python3
—called the shebang — as the first line in the circle.py
file, the shell will send all the other lines to python3
, which will be able to execute it. Thus we can run, in the command line,
./circle.py 1 1
3.14159265359
Modern Python development with uv
So far we’ve been writing simple scripts. But real Python programs often need external libraries —packages written by others that we can use in our code. This is where Python package management traditionally gets complex. Fortunately, uv makes it simple.
Self-contained scripts
One of uv’s most powerful features is the ability to create self-contained scripts with their dependencies declared right in the file. This uses a standard called PEP 723.
Let’s create a script that fetches weather data:
#!/usr/bin/env python
# /// script
# dependencies = [
# "requests",
# "rich",
# ]
# requires-python = ">=3.11"
# ///
import requests
from rich.console import Console
from rich.table import Table
console = Console()
# Fetch weather data
response = requests.get("https://wttr.in/Barcelona?format=j1")
data = response.json()
# Display it nicely
current = data["current_condition"][0]
console.print(f"[bold]Weather in Barcelona[/bold]")
console.print(f"Temperature: {current['temp_C']}°C")
console.print(f"Feels like: {current['FeelsLikeC']}°C")
console.print(f"Condition: {current['weatherDesc'][0]['value']}")
Save this as weather.py
. Now you can run it:
uv run weather.py
Or make it executable to run directly:
chmod +x weather.py
./weather.py # This works if uv is in your PATH
The magic happens in the metadata block between # ///
markers. When uv sees this, it:
- Creates a temporary environment
- Installs the required packages
- Runs your script
All in under a second! No virtual environments to manage, no requirements.txt
files to maintain.
You can also add dependencies to an existing script:
uv add --script weather.py beautifulsoup4
This updates the metadata block automatically.
Creating a Python project
For larger programs, you’ll want a proper project structure. uv makes this easy:
uv init my-weather-app
cd my-weather-app
This creates:
pyproject.toml
- your project configurationREADME.md
- documentationsrc/my_weather_app/__init__.py
- your package.python-version
- pins the Python version
Add dependencies:
uv add requests rich
Create your main script in src/my_weather_app/main.py
:
import requests
from rich.console import Console
def get_weather(city):
response = requests.get(f"https://wttr.in/{city}?format=j1")
return response.json()
def main():
console = Console()
weather = get_weather("Barcelona")
current = weather["current_condition"][0]
console.print(f"[bold cyan]Weather in Barcelona[/bold cyan]")
console.print(f"Temperature: {current['temp_C']}°C")
if __name__ == "__main__":
main()
Run it:
uv run python -m my_weather_app.main
Or add a proper entry point to pyproject.toml
:
[project.scripts]
weather = "my_weather_app.main:main"
Now you can run:
uv run weather
The development workflow
When developing with uv:
- Quick scripts: Use inline metadata for single-file scripts
- Projects: Use
uv init
for multi-file applications - Dependencies:
uv add
for projects, metadata for scripts - Running:
uv run
handles everything automatically
No more virtual environment activation, no more pip conflicts, no more “works on my machine” problems. Your scripts and projects are truly portable.
Programming Python
By now you have learned a great deal about the shell, and about the way it organizes and connects the many players in your computer. But we have only scratched the surface of Python. My goal was not to teach you the language, but to help you understand what a Python program is, and how it plays along with the rest of the world.
Python the language is beyond the scope of this book, but learning more about it is highly recommended. Knowing Python will greatly increase the types of problems that you can solve. For example, Python can be used to analyze large datasets, create visualizations, build web applications, and automate complex tasks.
Complex data analysis problems require knowledge of a programming environment. Languages like Python or R are excellent choices for statistical analysis and data visualization.
There are plenty of excellent resources online to learn Python. Many universities and online platforms offer comprehensive Python courses for beginners and advanced users alike.
Compiled languages and interpreted languages
When you were talking to Python, the python
program was always an intermediary between you and the computer. You ask for something in a language that you and Python understand, then python
makes sure that it is translated into machine code —the language that your computer actually understands— and then run. There translation to machine code is not free: you have to have python
doing it, and it takes time and machine resources that you could be using to actually run your program.
Languages that do that —stand between you and the computer, translating as you go— are called interpreted languages. This is usually not a problem. For most programming tasks the performance that you get from Python is plenty enough, and the ease of use and convenience of being able to talk a directly to the interpreter more than makes up for the loss of speed.
In some cases, though, you need more performance than what an interpreted language like Python can give you. The solution, then, is to write all or part of the program in a language that can be translated directly to machine code, thereby removing the need of an interpreter during the execution. This translation to machine code is called compilation, and the king among compiled languages is C.
A little C program
Developed by Dennis Ritchie in 1972 in order to write UNIX (which he also invented), C is a rather small language with which most of UNIX is written. All the tools we’ve been talking about (including Python) are written in C. The book The C Programming Language1 that Dennis Ritchie and Brian Kernighan wrote is one of the best computer science books ever, and one you should certainly read.
I will not try to teach you C in this book, but I want to give you a glimpse of how it looks like, and what the compilation process entails.
Make yourself a directory, and move into it.
mkdir ctest
cd ctest
Open a file called greeting.c
, and write the following in it:
#include <stdio.h>
int main(void)
{
int first = 10;
int second = 20;
printf("Hi there. Looks like %d times %d is %d\n",
first, second, first*second);
return 0;
}
There are several interesting things to note. First, the need to include things, just as you did in Python. The stdio.h
header has the definition of many functions, including the printf
that we’ll use in the program.
Second, the use of curly brackets to mark blocks (while in Python it was indentation). Even though it is not needed, we still indent blocks because it makes the code much more readable.
Third, the need to define your variables. You have to be explicit about what variables you want, and what type they are. The two we’ve defined here, first
and second
, are integers.
Third, the syntax in the printf
is something we haven’t seen before. First a string with control codes that look like %d
, then a list of values (in this case, first
, second
, and first*second
. The values in the list will replace the control codes, so we need the same number of both.
And fourth, and most important: you cannot run this program. At least, not yet. First we need to compile it. Run this:
Now this is where you might have a problem if you are on a Mac. By default, macOS does not install the command-line developer tools, and the C compiler comes with them. The easiest way is to run:
xcode-select --install
A dialog will pop up asking you to install the tools. Say yes - you’ll be using them all the time.
Once cc
(the C compiler) has finished with your file you’ll find an a.out
file in your directory. This is the compiled program, already made executable. You can run it directly,
Hi there. Looks like 10 times 20 is 200
Disclaimer: (I think I) get a cut from your Amazon purchase. Thank you very much for your support. ↩︎