Getting started with Python on FreeBSD => why & how!

Hi gang!

Editorial

Fun fact: I've been using FreeBSD for quite a while now and to be perfectly honest with you lot I'm (still) quite passionate about the whole thing. Always have, always will be. At the time of writing my backup VPS server is ("was") building 15.0-RELEASE (source hosted on my main server for security reasons). And speaking of said main server... well, today I've reached a personal milestone: Java no longer really exists on my server(s). No more JDK ("sorta"), no more (home brewed) Java programs ("class files") which perform certain maintenance functions, no more 'weird' folder structures to support a (somewhat) limited design for packages.

SO => 'Duke' ☕has been (officially) replaced by (a) Python 🐍, surely a much better companion for a daemon? 👿

Anyway, I've had the intention to look deeper into Python for quite a few years, and last year... it actually happened! The more I learned the more excited I became and well, here we are. Time for some advocating! ;)

So why Python?​

Just so we're clear? => I'm not a professional developer, just a hobby (Java) programmer who learned Python and got quite passionate about it. In addition I'm also quite familiar with shell scripting (obviously), VBA, C# and ASP.NET.

Interpreted language
(and in my opinion also more accessible than lang/perl...)

Python, just like Perl (and any other shell) isn't just a programming language, but also an interpreter. Meaning? Well...

Code:
peter@zefiris:/home/peter/temp $ python
Python 3.13.13 (main, Apr 11 2026, 21:11:22) [Clang 19.1.7 (https://github.com/llvm/llvm-project.git llvmorg-19.1.7-0-gcd7080 on freebsd14
Type "help", "copyright", "credits" or "license" for more information.
>>> name = "ShelLuser"
>>> print(len(name))
9
>>> print(type(name))
<class 'str'>
>>> print(name)
ShelLuser
>>> dir(name)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'removeprefix', 'removesuffix', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
>>>

As you can see: this means that you can easily start it, after which you get onto a new command line from which you can do "Python stuff".

But that's not what makes this feature so interesting: this also means that you can easily add a shebang to your scripts (you know, something like: #!/usr/local/bin/python?), set the execution bit ( chmod +x myscript.py) and then you can just run 'm from your shell as if it were a 'real' program. Which obviously makes it a lot more usable and accessible for automating administrative processes rather than having to manually start runtime all the time (which you normally always need to do with Java).

However, don't be fooled by this simple looking example... even though Python can be awesome for coding scripts it's still a full fledged OOP environment as well. So, you can easily utilize broader logic by using classes and the likes.

Re-using code is very easy (and logical!)
Let's say I coded a script called getStats.py. It has a function called "usersOnlineNow()" which doesn't only list all currently online users, it also recognizes aliases of my closest friends and actively filters on them. So this might be something I want to re-use in some of my other scripts. When dealing with Java you're now getting into the territory of (virtual) packages which can become annoying to use because they rely on specific folder structures which represent the whole package (and its domain). For example: lan/intranet/getStats.java.

With Python otoh you can easily do this: import getStats, right from within the same folder where the original script resides. Or maybe: from getStats import usersOnlineNow, this would only make the function 'usersOnlineNow()' available in my new script; so everything else in getStats would be ignored.

And if you want to create (virtual) packages to combine several of your modules? Also easy: just create a new folder, optionally add a file called __init__.py to provide some documentation (and maybe some optional initialization routine(s)). Then add your modules (so: "the scripts which contain functions that you plan to re-use in other scripts"), and done! Now you can simply 'import' the folder name, or import individual modules or, as shown earlier, individual functions.

Documenting your code is really easy
In my opinion it's good practice to comment your code and/or shell scripts. Unfortunately many programming languages rely on other tools to take full advantage of such documentation. For example an IDE which can show you what a certain function or class is supposed to do. Well... Python supports this right out of the box, while also providing a nice tool which allows you to take more advantage of it.

First things first... Python has its own help system (!) which might not be a full substitute for reference documentation, but it can still be quite useful:

Code:
>>> help
Welcome to Python 3.13's help utility! If this is your first time using
Python, you should definitely check out the tutorial at
https://docs.python.org/3.13/tutorial/.

Enter the name of any module, keyword, or topic to get help on writing
Python programs and using Python modules.  To get a list of available
modules, keywords, symbols, or topics, enter "modules", "keywords",
"symbols", or "topics".

Each module also comes with a one-line summary of what it does; to list
the modules whose name or summary contain a given string such as "spam",
enter "modules spam".

To quit this help utility and return to the interpreter,
enter "q", "quit" or "exit".
However, this also leads to the question: where does this documentation come from? If you ask help about, say, the print() function then how does it get all that information? Well, from so called docstrings. A docstring is a line which is surrounded by three double quotes: """This is an example of a docstring in Python.""".

So here's where things get more interesting: if you provide such a docstring below any kind of definition then you'll automatically document whatever object you just defined. So... whenever you create (or define) a new function, (global) variable, class or a method: one docstring is all you need to provide documentation. And the best part? You can either use oneliners, or write out a small summary, there are no strict rules here.

And all those docstrings can then be automatically used by Python. Either within its help system (as shown above), or when using the pydoc utility: this can parse any Python script and print (or save) a summary, either in text or HTML format. Oh, and that pydoc utility? It's actually a Python script of its own ;)

Of course there are many more (awesome) reasons to start using Python (like being able to run a debugger straight from the command line!), but these are simply my personal favorites.

How to get started?​

Well, as one could imagine: Python is fully included (and supported) within the ports collection. At the time of writing you can pick between Python 3.10 all the way up to 3.14. SO... if you plan on building Python yourself then do take note of /usr/ports/Mk/bsd.default-versions.mk; right now the default version is 3.11, but you're fully free to use another of course. Just make sure to define this in your /etc/make.conf.

Which then begs the question: "What version should you pick?". When in doubt then I'd stick with the default, however... it might be useful to start by checking the active Python releases. Right now 3.12 seems like a more logical choice to me, also considering that it'll be supported for another 2 years. Or, if you want to go a little more "bleeding edge" but without the actual risks then 3.13 is also a solid choice.

Once you have this out of the way then it's time to install your pick. In addition I also suggest installing lang/python-doc-text and/or lang/python-doc-html because not only does this provide some solid information, it also includes the official Python tutorial which, in my opinion anyway, can be an awesome help & reference. Fun fact: this tutorial also seriously helped me out to get started with Python.

Oh, and of course you can also install binary packages if you'd like: # pkg install python313 python-doc-text for example.

Libraries

As you might know Python is a very popular programming language, and there are also a lot of people who spend their time coding API's and libraries which can help to make our lives a little easier. Many of these libraries are also available in the ports collection. Some examples:
  • www/py-scrapy => Solid web crawling framework which can make it (very) easy to build your own crawlers.
  • security/py-gnupg => Fancy utilizing GnuPG in your Python scripts? Well, this could be a great help for that.
  • devel/py-gitpython => Want to interact with Git to automate a few tasks? This could help...
  • www/py-playwright => Need to automate the testing of a website? This library can provide some awesome help.
The thing is though... while these libraries are certainly useful, there are other (and sometimes better) ways to utilize these. Of course, context is a thing here; what works for me might not necessarily work for you.

When you install some of these libraries then they become available system-wide: so, accessible for all your scripts. Usually this isn't much of a problem, but what if you need a specific version of a library, or maybe another variant? Or what if... some names overlap?

PIP
In case you're wondering: Pip Installs Packages ;)

Python has its own package manager called PIP, and as you might have guessed: it's even written in Python itself. And PIP can also be used to install packages like the ones I mentioned above. By default PIP is only accessible on FreeBSD once you set up a so called virtual environment:

Code:
peter@zefiris:/home/peter/temp $ pip
/usr/local/bin/ksh: pip: not found
peter@zefiris:/home/peter/temp $ python -m pip
/usr/local/bin/python: No module named pip
peter@zefiris:/home/peter/temp $ python -m venv fbsd-test
peter@zefiris:/home/peter/temp $ cd fbsd-test/
peter@zefiris:/home/peter/temp/fbsd-test $ . bin/activate
(fbsd-test) peter@zefiris:/home/peter/temp/fbsd-test $ python -m pip --version
pip 26.0.1 from /home/peter/temp/fbsd-test/lib/python3.13/site-packages/pip (python 3.13)
(fbsd-test) peter@zefiris:/home/peter/temp/fbsd-test $
So what is happening here... by using the venv module I told Python to set up a so called virtual environment: this is a separated environment with its own Python interpreter and its own Python libraries (or "modules"). And because it's separated you can install (and use) whatever library you want and without the risk of possibly disrupting other projects. For example... there are many good libraries available for cryptography, but I've become quite keen of pycryptodome:

Code:
(fbsd-test) peter@zefiris:/home/peter/temp/fbsd-test $ which pip
/home/peter/temp/fbsd-test/bin/pip
(fbsd-test) peter@zefiris:/home/peter/temp/fbsd-test $ pip install pycryptodome
Collecting pycryptodome
  Downloading pycryptodome-3.23.0.tar.gz (4.9 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.9/4.9 MB 43.8 MB/s  0:00:00
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Preparing metadata (pyproject.toml) ... done
Building wheels for collected packages: pycryptodome
  Building wheel for pycryptodome (pyproject.toml) ... done
  Created wheel for pycryptodome: filename=pycryptodome-3.23.0-cp37-abi3-freebsd_14_4_release_p1_amd64.whl size=1726115 sha256=85e8503b8cca046108f081d42c82f533c88e3151c4dc9d69aee5d53198ee9263
  Stored in directory: /home/peter/.cache/pip/wheels/29/eb/c7/c569c89bdc7331f61e744a1847d02798ce31bf1bd1cb13cb33
Successfully built pycryptodome
Installing collected packages: pycryptodome
Successfully installed pycryptodome-3.23.0
PyPi working its magic here :cool:

So now I have access to all the features of pycryptodome, but only within this virtual environment, and also only when it's activated (noticed the change in my prompt btw?):
Code:
(fbsd-test) peter@zefiris:/home/peter/temp/fbsd-test $ python -c "import Crypto"
(fbsd-test) peter@zefiris:/home/peter/temp/fbsd-test $ deactivate
peter@zefiris:/home/peter/temp/fbsd-test $ python -c "import Crypto"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
    import Crypto
ModuleNotFoundError: No module named 'Crypto'
See what I mean?

And now?
Well, if you followed my tutorial up to this point then you should have everything installed to start using Python. And the best way to learn... is to actually start using it. So... why not build yourself a Python script (remember the shebang!) and then try to make it "do" something? Seriously, the best way to learn a programming language is to simply start using it.

And there you have it!

Thanks for reading, I hope you guys found this useful.
 
(Didn't realize this was a howto, sorry for replying)

Very nice written!

I think Python is a lot of fun and with tkinter you can do GUI stuff as well if you just want a small local app.

I noticed you have gotten rid of Java - does that mean you don't use devel/pycharm as IDE :) ?

/grandpa
 
I think Python is a lot of fun and with tkinter you can do GUI stuff as well if you just want a small local app.

I noticed you have gotten rid of Java - does that mean you don't use devel/pycharm as IDE :) ?
When it comes to scripting / building for my servers then I basically rely on 2 editors: VS Code for my efforts on Windows and I'm also a die-hard vi user on the console (even up to a point where I have vi-mode enabled on my shell). So.. never used Pycharm, especially not on my servers (which don't use X11 at all).
 
I think Python is a lot of fun and with tkinter you can do GUI stuff as well if you just want a small local app.
Are you aware that tkinter is tk with the whole tcl inside python?
Why python with the whole tcl inside if you can do the same with tcl?

And I do not think installing is a problem, more interesting would be a short introduction to python, or well, to tcl.
 
Are you aware that tkinter is tk with the whole tcl inside python?
<snip>
And I do not think installing is a problem, more interesting would be a short introduction to python, or well, to tcl.
I agree, but in all fairness the focus of the forum is of course on FreeBSD. Still, now that it's fully clear that this thread specifically addresses Python I don't see any reason why I wouldn't be allowed to expand a little bit on the learning process later on ;)

But to answer your question here... I actually already gave an implied tip on how you could start with this. Recall my comments about Python's internal help system and also pydoc?

To be perfectly honest with you: it's only been a few days since I started digging into Python's graphical options myself (so: tkinter in specific), and to make this even 'worse' my servers don't have any graphical support (so Python's GUI options won't initialize) and I only mess around with a graphical interface on Windows.

Even so: if you run this command you'll get a full introduction for tkinter, and even some example code to set up your own "Hello World": pydoc tkinter |more.

And if you just want a quick peek into what tkinter would look like: python -m tkinter.

"To be continued".
 
Recall my comments about Python's internal help system and also pydoc?
Internal help have sense when the interpreter not only run in UNIX like systems with man command.
Also R has such an internal help system. It makes also sense because one interacts with the interpreter, not only program.
tcl, although it is also run in Windows, is fully documented in its man pages.

By the way, R is really worth to learn, the programming language is interesting, the lots of functions and packages very usable not only for statistics. It is not a scripting language more, as I would qualify python. It also embeds tcl/tk for GUI.
 
Welcome to the dark side...Python: the language I hate to love. LOL Virtually everything I do anymore is rapid-app-prototyped in python, then converted to c++ if need be, but usually the python app works well enough as-is...and python has bindings to just about every open source library in existence.

These are some of my favorite python shortcomings:
1) GIL means that python will never have true cpu bound concurrent multi-threading...attempts in that direction are pipe dreams
2) languages that are "indentation sensitive" make me grind my teeth.
3) implicit declaration of vars can/does lead to subtle errors...ALWAYS explicitly define your vars just as a safety-practice
4) the type-neutral nature of objects can lead to subtle side-effects that you always have to be on the lookout for
 

Part II => Putting things to some real use​

Hi gang,

Well, I promised that I was going to do a follow up, and now seems like a good time as any. Today we're going to dive more into Python and we'll take a closer look at how we can use it to "do" things on FreeBSD. We'll take a (brief?) look into the language itself, talk about some of the standards (and 'PEP') and I think that building ourselves a calculator can make for a fun example.

OOP: Object Oriented Programming

Before we're getting into the good stuff there's one very important detail I want to address first... Learning a specific programming language in todays world isn't necessarily the most important part anymore. I'm not kidding: what do languages like Java, Perl, C# and of course also Python: what do all of these have in common? Well, obviously OOP; otherwise I wouldn't make such a big fuss about this, eh? My problem here though is that OOP isn't just a fancy standard, it's a way of life when working with development: just as much as it is a standard it's also a thought process which you can use to help optimize your work.

The only problem.. it's also rather abstract and when you look it up you'll get bombarded with buzzwords. No, there's merit to those words but if you're new(ish) then I can easily imagine that it'll be a lot of "blah, blah, blah..." => "c'mon: show me the good stuff already!". Not to mention that I've met plenty of veteran programmers who still sometimes struggle a bit with all this.

So what IS OOP?

The kitchenware factory
So... imagine that we have this awesome kitchenware factory: they make forks, knives and spoons. The factory actually started out small, at first they were only making knives and it was a solid process: first there were some machines that made the handles, and also some machines which made the blades. Add those together and ... easy!

Eventually they expanded and started to produce forks and spoons as well, but ... where to start? They wanted to start producing as quickly as possible and figured that you probably shouldn't change a working formula. So they simply copied the process to make the handles and set that up in the new sections during which other teams started to make the machines to produce the fork and spoon heads.

This worked out perfectly! Soon they had 3 separate sections in the factory where knives, spoons and forks were made and because all three sections followed the same design pattern and also roughly the same process it was even quite easy to get new staff up to speed too.

...and then the brand name changed.

So now they had a small problem on their hands. You see: the company always engraved the company name in the handles. Yet now they had 3 sections where these handles were produced so now they had to redesign all 3 different machines. They didn't have enough staff on hand to change all of them at once, but the demand for their stuff also made it quite costly to just stop the full production line.

Here's the thing: they could also have opted to use one machine (or section!) to make all those handles and then distribute those to all the other areas which made the rest of the parts. Sure, the moment they wanted to make forks and spoons they would also need to upgrade ("expand") the production of handles, but even so: it could have saved them a lot of hassle in the longer run.

Copying the design was easier at that time, but ... it also led to a bit of an impasse. Of course this is a very simplistic story example; but that's not the point. My point here is to try and help you better understand some of those abstract OOP concepts. And to realize the importance of re-usability.

And believe it or not, but this is something you'd actually be facing when working with an OOP programming language like Python.

Now for some Python...​

One of the main reasons why I love working with Python is because of its flexibility. It's a full fledged OOP environment, yet generally speaking it doesn't really force anything on you. For example... when you start working with Java the first thing you need to do is define a class, then a method... if you want something simpler to begin with.. then you should have picked another language.

With Python otoh. you can literally start working on a simple script project, slowly expand the project with some modules (= "reusable code"), even interim ones! And when push comes to shove... it's also easy to define packages ("module collections"?) and start working with full on classes whenever you need it.

A calculator script

Let's start with a small, simple, script example which adds up 2 numbers. Well... it tries to:
Python:
#!/usr/local/bin/python

a = input("Please enter a number: ")
b = input("Please enter a 2nd number: ")
c = a + b

print("The sum of your numbers is: " + c)
I left the 'shebang' in with this first example, but from now on I'll only be showing the actual Python code. This is yet another example of why I really enjoy working with Python on FreeBSD: I just set up the script, give it an execution bit and I'm all set:

peter@zefiris:/home/peter/temp $ ./calc.py
Please enter a number: 5
Please enter a 2nd number: 20
The sum of your numbers is: 5 20
Weird, huh? The script seems simple enough I think: I'm asking the user to enter 2 numbers and then I just add them up for them. I think it's logical enough, but as you can see it doesn't quite work as I want.

Now, to me it's obvious what is going on here: for "some reason" Python didn't treat my variables like numbers but as text. It literally added the text (or: string) '20' to the other text of '5'. Resulting in the literal mention of "5 20".

Variables & 'casting'

As simple as that script may look to you, there's actually a lot going on in there:
  • We can use variables to 'store' some kind of value for us which we can then re-use later.
    • BUT: variables in Python get assigned dynamically. In other words: I don't have to first define a type for them like integer, string, float, etc. but instead I can just assign a value and let Python handle the rest.
  • When you use "Python commands" you're mostly using internal ("builtin") keywords, functions and methods. That input() line? Well, you're actually using ("calling") a so called function, and functions often return a specific type of variable.
    • As mentioned before: variables can be of a different type depending on what we use 'm for. Integer implies that you're working with whole numbers, a string would imply that you're working with text whereas a float means that we're working with numbers that have a decimal point; so 'extra value' behind the comma.
Now let's get back to our small calculator... The solution seems simple enough: we just have to tell Python to treat our inputs as numbers, integers:

Python:
a = input("Please enter a number: ")
b = input("Please enter a 2nd number: ")
c = int(a) + int(b)

print("The sum of your numbers is: " + c)
In case you're wondering: int() is an actual class but it's a little bit too early to be talking about those right now. What we're doing here is often described as casting: we're changing one type of variable straight into another! We already determined that input() sends back string ("str") type variables. So we're now simply telling Python: "Ey, turn that into a number!" (or: integer).

Now, 2 things... remember my spiel about OOP and designs and what not? See, I could have handled this issue in different ways. I could also have simply used int() in combination with input() to make sure we'd always be working with numbers. Yet I didn't, and there's a good reason why: KISS. In other words: "Keep It Simple, Stupid". In combination with OOP design, of course.

My design here is quite clear: I have one section in my script which handles the input, and then another (even if this is but one single line!) ... so then I have another 'section' which "handles" the value(s). In other words: even in these early stages I try to keep things "separated", which makes it easier to read but also easier to "fix" or "enhance" if/when needed later on. You'll see.

Houston, we have a problem!
peter@zefiris:/home/peter/temp $ ./calc.py
Please enter a number: 5
Please enter a 2nd number: 20
Traceback (most recent call last):
File "/home/peter/temp/./calc.py", line 7, in <module>
print("The sum of your numbers is: " + c)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~
TypeError: can only concatenate str (not "int") to str
So the good news is that we obviously succeeded: we're now clearly working with numbers ("integers"). The bad news, as you can see here, is that we now have a new problem with our print() statement because we're now trying to "add" (or combine?) strings and integers together, and Python doesn't seem to like this very much.

The formatted string ("f-string")

Fortunately for us there's an easy fix here, and that's called the f-string, another one of my favorite Python features. When you're working with literal string values (so: any text that's between double quotes) then there's an easy way to tell Python that it should allow us to embed variables within our line of text.

And we do that by defining this as an "f-string", it's really super easy:
Python:
print(f"The sum of your numbers is: {c}")

So: just adding the letter f right in front of my string allows me to embed variables between curly brackets, and that tells Python to parse those variables and, well, display them. The result should be obvious:
peter@zefiris:/home/peter/temp $ ./calc.py
Please enter a number: 5
Please enter a 2nd number: 20
The sum of your numbers is: 25
Neat, huh?

Never make assumptions though!​

Once again we're diving into something that's not specifically related to Python but rather coding in general, but even so... Python does provide us with some pretty nifty tools to help us out.

Anyway, I proudly showed off my script to a friend of mine and sure enough... the first reaction I got was: "it's bork!!". When asking what was wrong they told me that "it doesn't work properly" which is weird, because ... well, look above? Works perfectly, right?

PS D:\temp> py .\calc.py
Please enter a number: 5
Please enter a 2nd number: twintig
Traceback (most recent call last):
File "D:\temp\calc.py", line 3, in <module>
c = int(a) + int(b)
~~~^^^
ValueError: invalid literal for int() with base 10: 'twintig'
In case you're wondering: "twintig" is Dutch for "twenty". Oh, dear... Window users, right? ;)

But it does raise a fair point I think: error handling, we basically assumed that a user would only input numbers. But what if they don't? Fortunately for us we have plenty of options to fix this. You see... when you're working with an OO language then most of the "items" you work with are actually virtual objects, and objects tend to have plenty of "members" to 'handle' them further.

In other words: the moment you define a string ("str") you basically have a "string class" on your hands, which also provides plenty of methods to "do" something with that string. How about... using isnumeric() to help us check if we're actually working with numeric values.. We'll just use some "if statements" to check, and we'll be all set!

Python:
a = input("Please enter a number: ")

if (a.isnumeric() == False):
        print("You didn't enter a valid number, please try again...")
        exit()
else:
        b = input("Please enter a 2nd number: ")
        if (b.isnumeric() == False):
                print("You didn't enter a valid number, please try again...")
                exit()
        else:
                c = int(a) + int(b)
                print(f"The sum of your numbers is: {c}")
Technically speaking... this script 'works'. But the harsh reality is that this script has actually become a complete mess. Remember my story about the factory and those different machines which all the did the same thing? Well, that's what you see happening right here as well.

But there's also something to learn here. As you can see I made sure to apply indentation in my code. I didn't do that because I think things look better (I actually do, but that's besides the point), no: the main reason I did this is because this is mandatory with Python. If you want to make it clear that some code should be grouped together... then you need to use indenting.

Even so... it may look nice, but as said: this code is no good.

A (desperate!) need for functions!

First: if you see the same lines of code get repeated and re-used multiple times then that's usually not good. I mean, what if we want to change this code somewhere in the future? What if we're actually going to accept words and parse those? Then we'd have to rewrite the whole routine again, multiple times even! And that's hoping that we'll still remember that some routines were copied.

Second problem: this code is also very hard to read right now, when you start 'nesting' checks very deeply then you should probably look for cleaner solutions. I mean... "If 'a' isn't a non-numeric value then we'll check if 'b' isn't a non-numeric value after which we'll do the calculation". It's just messy: "if a is not, not a numeric, then...".

Not to mention that we're doing the exact same check on both variables; there is no excuse here: that code snippet needs to be separated so that we can more easily re-use it. And the best way for that.. is to use a function. In fact... while we're at it, why don't we do the same thing for the whole input routine as well?

Python:
def ask_number(vraag):
        x = input(f"Please enter {vraag}: ")
        if (x.isnumeric()):
                return int(x)
        else:
                print("You didn't enter a valid number, please try again...")
                exit()

a = ask_number("a number")
b = ask_number("a 2nd number")

print(f"The sum of your numbers is: {a + b}")
See what I mean? I pretty much managed to cut the script in half. Not only that: it's much easier to read now as well. By using 'def' I told Python that I wanted to define a function: a separate snippet of code with its own name, a name which I can then re-use later on. A bit comparable to functions in shell scripts.

Because I'm using a string to ask the user for input there's no reason why I wouldn't turn that into an f-string as well, this allows me to set up the question a little more dynamically.

But there's more: my "ask_number()" routine ("function") also looks like something I might also be able to (re)use in another script of mine. You see.. here I am adding up 2 numbers, but I've also been secretly experimenting with... (drumroll) => subtractions! :rolleyes:

Now, I suppose I could just copy&paste that function snippet right into my 2nd script but... copying & pasting isn't exactly the best practice while coding. Instead, I'm just going to tell Python that I want to use this part of my original script:

Python:
from calc import ask_number as ask

a = ask("first number")
b = ask("second number")

print(f"{a} minus {b} equals: {a - b}")
So now I have 2 scripts: calc.py (which I showed earlier), and I made this second script called subtract.py and placed it in the same folder. First I told Python that I only want to use the function in this script, and it should be named "ask".

Let's see what happens:

peter@zefiris:/home/peter/temp $ ./subtract.py
Please enter a number: 5
Please enter a 2nd number: 4
The sum of your numbers is: 9
Please enter first number: 5
Please enter second number: 4
5 minus 4 equals: 1
That's no good... now it somehow ran both script in sequence. But that's not what I wanted?!

Making our first module

But no worries, once again this makes perfect sense. You see... by using import I literally asked Python to load and parse my other script so that it can pick up on this method. However... my script is an actual script which also "does" something right after you start it. Yet in this case I actually want to use it more like a module rather than a script. In other words: when I'm using 'import' then it doesn't need to "do" anything, all I want is to re-use its main function.

Fortunately I can... you see, there's a little trick involved with a system variable called __name__. This is a system ("builtin") variable which gets the name of the main routine ("session") which is running the scripts. When I "just" fire up a script then this variable will get a very specific name: __main__. But when you re-use a script by using import then all of a sudden this variable gets the name of the currently active script, because at that time that's the currently active session.

Let me show you what I mean... I'm going to add a 2nd print statement within my calc.py script:

Python:
print(f"The sum of your numbers is: {a + b}")
print(f"My name is currently: {__name__}.")
Now let's see what happens:

peter@zefiris:/home/peter/temp $ ./calc.py
Please enter a number: 5
Please enter a 2nd number: 4
The sum of your numbers is: 9
My name is currently: __main__.
peter@zefiris:/home/peter/temp $ ./subtract.py
Please enter a number: 5
Please enter a 2nd number: 4
The sum of your numbers is: 9
My name is currently: calc.
Please enter first number: 5
Please enter second number: 4
5 minus 4 equals: 1
See? First it's __main__, but then it turned into calc.

Now this is something which we can use to ensure that our script only gets fired up when we want it to. We're going to add another function and also a little check:

Python:
def ask_number(vraag):
        x = input(f"Please enter {vraag}: ")
        if (x.isnumeric()):
                return int(x)
        else:
                print("You didn't enter a valid number, please try again...")
                exit()

def _main():
        a = ask_number("a number")
        b = ask_number("a 2nd number")

        print(f"The sum of your numbers is: {a + b}")

if (__name__ == "__main__"): _main()
See? This will make sure that the routines in our calc.py script will only get fired up when we actually start the script itself. But the moment this script gets parsed from merely using an import statement somewhere else then this _main() function will get fully ignored, simply because the __name__ variable doesn't match anymore.

What's with all the _'s?​

Last but not least... you've seen a lot of _ characters being used so far, and there's actually a good reason for all that. Whenever a variable is surrounded by 2 underscores (like __name__) then this indicates that you're working with a system variable. So something build into the Python system itself. Some other examples are: __doc__, __file__, __package__, __spec__, and so on. In case you're interested then the dir() function is an awesome way to study this a little bit more.

Second... as soon as you use a single _ character in front of a name then that indicates that we're dealing with a "hidden" item. It's not really hidden of course, but the name indicates that it's something which should only be used within the current "scope" itself.

In other words: In my previous example it is perfectly fine to import ask_number() but trying to import _main() would be frowned upon as bad practice because its name indicates that it wasn't meant to be used like that.

End of Part II, "to be continued..."​

And that concludes part 2 of my tutorial. Thanks for reading, I hope this was useful for some of you.

In the next part we're going to be looking at documenting your code ("docstrings"), a better way to group some of our modules together using "packages", we'll be looking at some better ways to do "flow control" (there's more than asking "if... then") and we'll also briefly check out how we can debug some of our code. Any exceptions (lol)?

Anywhoo, see you next time ;)
 
Java:
$ jshell JAVASE
|  Welcome to JShell -- Version 25.0.3
|  For an introduction type: /help intro

jshell> var name="weberjn"
name ==> "weberjn"

jshell> name.length();
$184 ==> 7
 
Java:
$ jshell JAVASE
|  Welcome to JShell -- Version 25.0.3
|  For an introduction type: /help intro

jshell> var name="weberjn"
name ==> "weberjn"

jshell> name.length();
$184 ==> 7

Your point?

Python 3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> x="kent_dorfman"
>>> print (len(x))
12
>>>
 

Part III => Documenting, organizing and bug fixing​

Hi gang!

So, some of you reached out to me (thanks!) and asked if I could also provide some examples on how Python helped me to enhance my FreeBSD setup in specific. Yups, definitely going to dive into that as well. Probably not in this chapter perse, but it's on the todo!

Documentation: maybe annoying, but necessary!​

Good code is documented code in my opinion. Now, "back in the days" (uh, oh.. grandpa Shell talking! 🤭) ... when I started coding, all the way back using Borland Pascal, I picked up a strong opinion on documentation. I mean, surely the code itself was its own documentation? If you knew your Pascal then you should be able to see what's going on.

...and then I left one of my hobby projects alone for a few months; this was an expansion module for the Concord BBS. I dove right back in eventually and sure enough: I had to spend 30 or so minutes before I was fully back on track. Turns out: my style changed during those months and some techniques suddenly felt off to me. Maybe that documentation wasn't so bad afterall ;)

Annotations + docstrings = valuable (and easy!) info!
Python has a tool called pydoc which can help process any documentation that's within your code, and more... let's see what pydoc does with the calc.py script/module which we got so far:

peter@zefiris:/home/peter/temp $ pydoc ./calc.py
Help on module calc:

NAME
calc

FUNCTIONS
ask_number(vraag)

FILE
/home/peter/temp/calc.py
It clearly noticed that we have a module on our hands (= "a Python script with re-usable code"), and it also identified the function we defined in the previous chapter. However... while we can easily re-use this function it's not exactly clear what's going on.

By the way: noticed how pydoc automatically ignored any mention of the _main() function? Amazing what a simple _ character can do, right?

But yeah, there's plenty left to question here: does this function give us any kind of value in return? Its name suggests as much, but.. beats me! Also: what is "vraag" supposed to be? A variable for sure, but... maybe it's a number which tells the function how many values we want to get from the user?

Let's fix this!

Python:
"""
A script(/module) which I made as an example for a Python tutorial which I posted on
the FreeBSD community forum. You can find my tutorial here:

https://forums.freebsd.org/threads/getting-started-with-python-on-freebsd-why-how.102463/
"""

def ask_number(vraag: str) -> int:
        """Asks the user to enter 'a number' (= "vraag"), and returns the verified (!) integer."""
        x = input(f"Please enter {vraag}: ")
        if (x.isnumeric()):
                return int(x)
        else:
                print("You didn't enter a valid number, please try again...")
                exit()
See what I mean? First I made sure to specify that I expect 'vraag' to be a string ("str") by adding this behind the parameter. Then I added mention of 'int' after the 'arrow' to indicate that this is what my function is going to return. Do note that this does nothing for any functionality: I'm merely indicating ("annotating") that my function expects a string value, but this indication does not enforce anything. We'll get to that later.

Docstrings

And then the docstring. As soon as you place something between triple double quotes (lol! 😁) then it's recognized as a docstring. A 'simple' comment section which can be picked up by tools such as pydoc. As you can see you can easily use a docstring as a single line, but also use it to set up as a small documentation section.

Suddenly it becomes much more obvious what this calc.py file is supposed to do:

peter@zefiris:/home/peter/temp $ pydoc ./calc.py
Help on module calc:

NAME
calc

DESCRIPTION
A script(/module) which I made as an example for a Python tutorial which I posted on
the FreeBSD community forum. You can find my tutorial here:

https://forums.freebsd.org/threads/getting-started-with-python-on-freebsd-why-how.102463/

FUNCTIONS
ask_number(vraag: str) -> int
Asks the user to enter 'a number' (= "vraag"), and returns the verified (!) integer.

FILE
/home/peter/temp/calc.py
Here's the thing you guys... I mentioned a few times already that I moved on from Java and embraced Python. And what you see here is another solid example as to why I came to that decision. Back in the days I was a huge fan of javadoc: both a tool and a good workflow which could help you to document your code.

But this documentation goes deeper than just a simple tool!

peter@zefiris:/home/peter/temp $ py
Python 3.13.13 (main, May 10 2026, 15:57:06) [Clang 19.1.7 (https://github.com/llvm/llvm-project.git llvmorg-19.1.7-0-gcd7080 on freebsd15
Type "help", "copyright", "credits" or "license" for more information.
>>> import calc
>>> print (calc.__doc__)

A script(/module) which I made as an example for a Python tutorial which I posted on
the FreeBSD community forum. You can find my tutorial here:


>>> print (calc.ask_number.__doc__)
Asks the user to enter 'a number' (= "vraag"), and returns the verified (!) integer.
>>>
See? Documentation isn't just a simple add-on that is only used by some external tools. Nope! Docstrings are an integrated part of the language itself.

You do realize what this means, right? How about... adding a routine to check if our script is properly documented? ;)

Python:
from calc import ask_number as ask

if __doc__ == None:
        print("Script not properly documented!")
        exit(1)

a = ask("first number")
b = ask("second number")
print(f"{a} minus {b} equals: {a - b}")
Sure, this is obviously a silly example, but nevertheless it does exactly as you'd expect: the routine forces me to make sure that my current script is properly documented, and I like that. In fact... this is something I might to use in all my other scripts as well. Now, I already demonstrated how we could set that up: add a function and then we can re-use that as a module and import it later on within our other scripts.

The thing is though... all this is becoming a little bit chaotic. I mean... I have a few scripts that are actually scripts, I also have a script which has a function that I'm re-using and now I plan to make an actual module. It's fun and all, but... also a bit confusing.

Packages​

peter@zefiris:/home/peter/temp $ ls
__pycache__/ calc.py* check_doc.py subtract.py*
Now, this is a simple example of course, but as you can see it's still getting harder to tell which script does what... Is it a script or is it a module? Or both?! Sure: subtract is an actual script, but it relies on calc. And I guess both scripts will now rely on check_doc but... it's getting difficult to tell which script does what. I mean, even though calc has the exec bit set (notice the asterisk?) it doesn't imply that it's only being used as a script. Because we're importing that one function it also serves as a module.

We need clarity. Just like you want to avoid code repetition and keep such code in one place you should also be mindful about keeping all your re-usable code in a dedicated space. And if it can be helped: also group those modules together.

The solution? Well, the concept of packages. As its name suggests: a package is essentially a collection of one or more modules, neatly grouped together in a directory. SO basically... a directory with Python scripts. This is also something I enjoy about coding: while you're working on solving problems you're bound to come up with useful routines, and Python makes it very easy to store these in modules using packages.

So, let's set up a tools package:

peter@zefiris:/home/peter/temp $ ls . tools
.:
__pycache__/ calc.py* subtract.py* tools/

tools:
__pycache__/ check_doc.py vraag.py
First I grabbed the ask_number() function from calc.py and moved that to vraag.py which is now located in the tools/ directory. I also moved check_doc.py here as well, but we'll get back to that later.

And this also opens up many possibilities. Because... since both calc and subtract are going to use the same module it probably makes much more sense to combine these scripts. In other words: have a calc.py script which can do additions and subtractions at the same time.

Import (again) and the use of system libraries​

Python:
import sys
from tools.vraag import ask_number as ask

def add() -> int:
        a = ask("a number")
        b = ask("a 2nd number")
        return a + b

def subtract():
        a = ask("first number")
        b = ask("second number")
        print(f"{a} minus {b} equals: {a - b}")

arg = sys.argv
if (arg != None) and (len(arg) == 2):
        match arg[1]:
                case "+":
                        print(f"The sum of your numbers is: {add()}")
                case "-":
                        subtract()
                case _:
                        print("Error: invalid calculation method specified!")
                        print("(only + and - are supported at this time)\n")
                        sys.exit(1)
else:
        print("Error, you didn't specify a calculation method (use + or - as an argument).\n")
        sys.exit(1)
Now this is more like it I'd say! We can do additions and subtractions using this one single script, the routine which asks our users for input sits in its own module within a dedicated tools package so that we can re-use that part in other scripts. And of course: we made sure to have everything neatly commented as well.

As you can see the import command plays a crucial role here, and so I wanted to specifically mention it one more time. Also because it somewhat baffles me how many Python tutorials just seem to gloss over its use. Sure, it's a relatively simple command, but also extremely important and flexible:
  • You use import to reference other modules or packages(!), so that you can re-use their contents (= modules, functions, variables, etc.).
    • Fun fact: There's not much use for importing a package, unless you set up some automation (remember my mention of that __init__.py script?).
  • If you want to use aliases to make your code better readable you can use as to tell Python that you want to use a dedicated name for whatever you just referenced.
    • You can see this in the script above: I don't use the name 'vraag.ask_number' but instead renamed all this to ask.
  • And if you only want to reference ("import") a specific snippet then you can use the from statement.
    • This applies to modules and packages alike. In my above example I used this to only reference the 'vraag' module.

System libraries
I don't know about you, but this would definitely lead me to a question: where is that sys reference coming from?

Well, I sure hope you didn't think that those fine folks at the Python foundation just left us coders to (re)invent our own devices and mechanics. And no worries if you did, because then you'll be in for a treat!

python_syslib.jpg

I present you: the Python (3.13) system library on FreeBSD 15.0 (RELEASE).

This is another thing which I absolutely love about working with Python: most of its system libraries (not all of them!) are actually (stand alone) Python modules which you can find in its own directory within the /usr/local/lib folder (as shown above). And as mentioned: people mostly used Python itself to set all of this up.

If there's something which you want to "do" then there's a good chance that you'll find something useful in here. From accessing sqlite3 files, using JSON encoded data, accessing HTML and/or e-mail standards, utilizing encryption (!), processing zipfiles ("archives"), working with calendar functions, sorting out IP addresses, using OS specific functions....

Oh, fun fact: I'm sure you're using pkg to maintain your installed software on FreeBSD, right? You do realize that /var/db/pkg/local.sqlite is actually... an sqlite3 formatted file? While I would not recommend doing this casually (!) you could use Python to access that file to see what kind of data is really inside. We'll get to this later on.

The system library is open source(d)!

Most of the library files in here are actually regular Python modules ("scripts"?) which you can easily check out yourself. Want to see how Python handles the JSON format? Couldn't be easier, just head on over to: /usr/local/lib/python3.13/json/ where you'll find modules like encoder.py and decoder.py. Just view the file(s), read their comments ("docstrings") and maybe check out the functions (and/or methods) to see how it was set up.

Keep in mind though that not everything is part of this. In my previous script I used import sys, but if you look around the system library you'll find no mention of sys anywhere. That's because there are also some routines build into Python directly, the so called "builtins":

>>> dir()
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
>>> import sys
>>> print (sys.__spec__)
ModuleSpec(name='sys', loader=<class '_frozen_importlib.BuiltinImporter'>, origin='built-in')
>>> print(__builtins__)
<module 'builtins' (built-in)>
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BaseExceptionGroup', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EncodingWarning', 'EnvironmentError', 'Exception', 'ExceptionGroup', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError',
See what I mean? There are several routines which are fully OS dependent and as such... they are fully part of the system itself. But Python wouldn't be Python if it didn't clearly showcase this as such. Like I said earlier: there's a good reason why I really favor the dir() method, especially when you want to learn more about certain parts of Python.

The exception(s) to the rule(s)​

So... there I was. I was so happy with my new tools package that I even dumped it onto a local Git repository so that my friend(s) could use it too. Good times! Well... sorta, because obviously I got more (constructive!) criticism...

Just to remind you, this is what I'm talking about, tools.vraag:

peter@zefiris:/home/peter/temp $ cat tools/vraag.py
def ask_number(vraag: str) -> int:
x = input(f"Please enter {vraag}: ")
if (x.isnumeric()):
return int(x)
else:
print("You didn't enter a valid number, please try again...")
exit(1)
As you can see we exit() the routine whenever the user makes a mistake, because why wouldn't we? Well, my friend told me that it seems obvious that I don't understand loops yet (??) because they don't want their script to exit. Instead, they want to inform the user that they made a mistake and allow them to try again.

This obviously created a problem for us, because how do I tell them that there was a mistake? I suppose I could ignore my own annotations and instead of sending back ("return") an int value I could send back False (so: a boolean value), but that seems kinda inconsistent and also confusing. Not to mention that handling all that can quickly become a headache of its own.

Fortunately there's a simple solution for this... and it's called an exception.

An exception is a special kind of signal which you can send "up the stream" to raise awareness that something isn't going as planned. It exists 'outside' of the normal scope of sending data back and forth and it can also be handled as such on its own.

So the only thing left to do now is to decide what kind of exception I'm going to use... When I'm confronted with questions like that ("what to use?", or "how to...") then I usually take a quick peek at the PEP index, just to see if something already exists; and maybe I'll pick up extra information along the process as well.

However, in this case I ended up checking out the standard library guidelines on (base)exceptions and decided to use TypeError to indicate that the user didn't enter an actual number. I'm sure that even without telling my friend about this it will become obvious what is happening.

SO, let's set it up:

Python:
def ask_number(vraag: str) -> int:
        x = input(f"Please enter {vraag}: ")
        if (x.isnumeric()):
                return int(x)
        else:
                raise TypeError("User didn't enter a valid number.")
Well, you'll quickly notice that this definitely changed the way our calc script is now behaving whenever we make a mistake:

peter@zefiris:/home/peter/temp $ ./calc.py +
Please enter a number: een
Traceback (most recent call last):
File "/home/peter/temp/./calc.py", line 32, in <module>
print(f"The sum of your numbers is: {add()}")
~~~~^^
File "/home/peter/temp/./calc.py", line 17, in add
a = ask("a number")
File "/home/peter/temp/tools/vraag.py", line 11, in ask_number
raise TypeError("User didn't enter a valid number.")
TypeError: User didn't enter a valid number.
peter@zefiris:/home/peter/temp $
As you can see our script still quit, but this time it also spewed out quite a bit of extra information. This "stack trace" or better put: this traceback tells us exactly what is going on (= "TypeError") and where this is happening. It literally traces back the cause of the issue: starting with the use of the add() function within our script, then this gets traced further back to the use of the ask() function; and as we hopefully still remember: ask() is essentially an alias for tools.vraag.ask_number(), something which Python also easily detected. And eventually Python tells us that this all started in vraag.py, line 11.

Now, this is nice and all, but I obviously don't want our users to see all that those error messages, only our original error message will do.

As you can hopefully imagine there's obviously a solution for this as well: we need to try and 'handle' the exception, and we do this by literally 'trying' to use the command(s) and then check if anything went wrong. Trust me, it's easier than you may think:

Python:
arg = sys.argv
if (arg != None) and (len(arg) == 2):
        try:
                match arg[1]:
                        case "+":
                                print(f"The sum of your numbers is: {add()}")
                        case "-":
                                subtract()
                        case _:
                                print("Error: invalid calculation method specified!")
                                print("(only + and - are supported at this time)\n")
                                exit(1)
        except TypeError:
                print("You entered a wrong number, please try again!")
                exit(1)
else:
        print("Error, you didn't specify a calculation method (use + or - as an argument).\n")
        exit(1)
As you can see I placed my original code in a try section, so I'm pretty much literally "trying" to see if something happens. When it does I specifically reference the TypeError which we set up and handle that exception by printing my error message and then exiting the script just like I did before.

Now you might be wondering... isn't this overcomplicating things a little bit? I mean, what practical use does this all have if you only want to use your simple (?) Python scripts? Well...

peter@zefiris:/home/peter/temp $ ./calc.py +
Please enter a number: ^CTraceback (most recent call last):
File "/home/peter/temp/./calc.py", line 33, in <module>
print(f"The sum of your numbers is: {add()}")
~~~^^
File "/home/peter/temp/./calc.py", line 17, in add
a = ask("a number")
File "/home/peter/temp/tools/vraag.py", line 7, in ask_number
x = input(f"Please enter {vraag}: ")
KeyboardInterrupt
Python is a very consistent language, and these exceptions aren't something which you only use to indicate errors. Here I decided to press control + c to break off my script, and the result should be obvious: now the Python system itself (!) raised an exception because I pressed control +c, notice the mention of KeyboardInterrupt?

Guess what? We can now also easily handle this issue as well, simply by adding another except section where we reference this KeyboardInterrupt in particular:

peter@zefiris:/home/peter/temp $ ./calc.py +
Please enter a number: ^C

Quitting script, as requested.
Now our rather simple calc script is behaving much more professionally, wouldn't you agree?


End of Part III, we're still gonna continue ;)

Once again thanks for your time, I hope this was still useful and/or fun to read. And obviously (!) also thanks for your all feedback as well!

I'm well aware that I glossed over the 'match' flow control part, we're going to take a closer look at that in the next chapter. We're also going to take a better look at testing (and debugging) our code, because trust me: the time of using print() + exit() commands everywhere just to see what's going on should be long behind us. We can do that much more efficiently.

And assuming I'm not going to be sidetracked too much we're also going to take a brief (?) look at classes and what they're used for, in combination with paying some more attention to how all this stuff can help us with using / expanding and enjoying FreeBSD itself. While you shouldn't underestimate all the stuff you can do in a shell script (!), it's also fair to say that the use of a complete programming language has its advantages as well.
 
Back
Top