Pythonity Blog Pythonity Blog

Pythonity's Open Source Stack

Hi, how are you? Awesome-fantastically-great? Good. Sorry for the break but it's been busy. I know, I know, it always is, but sometimes it's really busy. Anyway.

If you follow our Twitter account (no pressure, but you really should) then you could see some action in the last couple of days. Nothing major, but we did a little bit of a um... winter cleaning for of our open source projects:

Some minor cleaning up here and there, adding some new tricks that I learned in the last couple of months. But probably most importantly, updating our Tox configuration to test the code against Python 2.7, 3.4, 3.5 and recently released 3.6 version, adding code style testing via flake8 and finishing it all of via integration with Coveralls. Oh, and did I mention that all of this happens automatically after each push and on each pull request with the help of the awesome Travis CI. Also - badges. Did you hear that? That was the sound of me arriving in geek heaven. I like it here.

And with all that, I though it's a good excuse to do a rundown of our open source stack.


Well, duh, but what I mean by that are Python versions. I'm a big Python 3 supporter and was lucky enough to start with Python around the time when Python 3 had the popular libraries support and starting a new project in it was a no-brainer. I've heard that it wasn't always the case : -)

So originally we supported Python 2.7 only on the most popular icon-font-to-png and decided to release other libraries as Python 3 only for a couple of reasons. I still believe it isn't necessarily a bad choice, but if a couple hours of work, extra UTF-8 shebang, couple of imports from the future and weird looking imports from six is all it needs to run on both Python 2.7 and 3.x then sure, why not. Also - universal wheels.

We decided to support Python 2.7, 3.4, 3.5 and 3.6 for the foreseeable future.

Project hosting

Absolutely nothing against GitLab (we self host it for our company projects!) and BitBucket but GitHub is where everything's (including Python in a little bit) at. Sorry, not sorry.

Test framework

We debated this internally for a bit and ended up on pytest. To be honest, more because the lack of any preference for the rest of the guys and my personal preference of it. By default, my favorite kind of winning : -)

But why pytest though?

Well, I don't have a really good answer for that, but I'm sure that someone on the internet has. For me it was the thing I started with and just never felt the need to change. So far so good basically. It works as expected with a couple of lovely features that I was surprised not to see in other frameworks when I occasionally work with them. And I will never understand the need for self.assertItIsMoreThenFourButLessThanTen stuff.

Test runner

Yeah, I don't know if the title is entirely correct because I'm sure that pytest is also a test runner. Sue me - I needed a paragraph to put Tox in.

While there are a couple of big and popular Python test frameworks, I think I always saw them being 'plugged into' Tox, which - if you don't know - automates your testing and allows you to run your tests in multiple separate virtualenvs. Practically a must have when you want to support multiple Python versions and make sure that tests pass on each of them.

Code style guide

PEP 8 guys, use it. And if you're using it, then why not test your code against it with flake8 plugged into you Tox configuration? Only good stuff can happen.

Code coverage

We all know tests are important. There's a bit more of divisiveness for code coverage as a code quality metric, but I think it's absolutely fine if you don't go mental about it and strive for 100% everywhere. At the very least, it can show you which edge cases (or even whole classes and/or functions ; ) you forgot about.

Again, I belive is the clear crowd favourite.

There are more options for showing the data online though. I played with Codecov for a bit but ultimately decided on Coveralls. I'm still new to it and had some problems setting it up, but everything else has been good so far.

Continuous Integration service

This category is actually quite stacked up, with most of the services allowing unlimited number of open source repos. We have Circle CI, Scrutinizer, Codeship, self hosted Jenkins, GitLab CI and many, many more. Lastly, we have Travis CI which is what we're using. Again, people much smarter than me already did all the comparisons and I'm sure all of these would work perfectly well for our small needs.

So many choices, so many possibilities. Like I said, geek heaven.

Coveralls with Travis CI, Tox and pytest

We have a proper, lengthy post coming real soon, but I just quickly wanted to tell you how to add Coveralls support to your Python project that runs tests via some sort of combination of Travis CI, Tox and pytest.

I recently went through that process and had a bit of a trouble - let's just say Coveralls docs aren't very helpful. The only thing they do is point you to one of python-coveralls or coveralls-python. I know, the choice is so obvious.

I decided to go with the more popular coveralls-python, which also seems to be more feature packed and for instance doesn't require any Coveralls configuration (like explicitly setting repository token) when run on Travis CI which is nice. I also found pytest-cov which sounded like exactly what I was looking for, but long story short - it wasn't.

Okay, so let's just use directly, make it do its thing. I know, let's add it as a separate Tox environment, similarly to flake8. Wow, so nicely done, right? Nope. If you think about it some more it becomes obvious why - Tox runs its tests in separated virtualenvs and the files created while running them aren't available for coveralls-python to find in Travis build directory. Duh.

So finally I came to the right conclusion - Tox is out of the scope and all stuff needs to be invoked directly in .travis.yml:

$ cat .travis.yml
language: python

  - "2.7"
  - "3.4"
  - "3.5"
  - "3.6"

  - pip install -r requirements/dev.txt
  - pip install tox-travis

  - tox
  - coverage run --source icon_font_to_png -m py.test
  - coverage report -m

  - coveralls

and just to get the full picture:

$ cat tox.ini
envlist = py27,py34,py35,py36,flake8

commands = py.test -v
deps = -r{toxinidir}/requirements/dev.txt
passenv =

commands = flake8 .
deps = flake8

python =
  3.6: py36, flake8

addopts =
python_files = *.py
python_functions = test_

exclude =

So there you go. Not really that complicated but worth putting online for future reference and saving that precious half an hour.

As always - let me know if you have any questions.

In Python 3.6 regular dicts are ordered and more compact

Busy times at the company so no long form post today, but I wanted to pass along this and this, as I found both the news and the discussion worthwhile.

Couple of my favorite comments:

"ordered" means "insertion order". For a sorted dict see the excellent sortedcontainers module. This provides dicts, lists, and sets that return keys in sequence (including, you can supply a key() func for custom sorting), and maintain the sequence under deletions and insertions, with low overhead. This functionality is still not in the std library.


This update broke my workflow! I was using dictionaries for an additional source of entropy in my RNG, and now I'll have to start over again.


Ordered dicts are a CPython implementation detail, and are not specified or guaranteed to be consistent across other forms of Python (although they will be ordered in PyPy as well). On the other hand, OrderedDict is always guaranteed to be ordered. On CPython, when you ask for an OrderedDict, what you're getting will essentially be a thin wrapper around dict with a few extra methods, so you should still have the speed of a native dict.


(...) dicts are not guaranteed to be ordered, but - keyword args are ordered - the namespace passed to a metaclass is ordered by definition order - ditto for the class dict A compliant implementation may ensure the above three requirements either by making all dicts ordered, or by providing a custom dict subclass (e.g. OrderedDict) in those three cases.


If you want to play with the newest release, Python 3.6.0 beta 1 is now available.

What's New in Python 3.5 and 3.6

Python 3.4 was released on 16 March 2014, more than two years ago but I've been using it for a while, mainly because it's the latest Python 3 available in official Ubuntu 14.04 repositories, which is our choice for almost all of our servers and on my previous desktop OS elementary OS 0.3 Freya by extension. Sure, there are custom user made PPA's, but adding it by hand everywhere didn't seem like a fun thing to do, especially since we don't (yet?) use the features that Python 3.5 introduced, and that Python 3.6 will introduce.

But two years is a lot and as I explained in my previous post I recently switched to OS X, installed pyenv and suddenly every Python version released was at my fingertips. So let's go through the best features in Python 3.5 and Python 3.6 and see weather a change is something you should also consider, especially with new Ubuntu LTS (16.04).

Python 3.5

PEP 492 - Coroutines with async and await syntax

PEP 492 greatly improves support for asynchronous programming in Python by adding awaitable objects, coroutine functions, asynchronous iteration, and asynchronous context managers. Inside a coroutine function, the new await expression can be used to suspend coroutine execution until the result is available. Any object can be awaited, as long as it implements the awaitable protocol by defining the await() method.

This is the big one, proper asynchronous programming in Python. I never got around to actually using it in a project, but I recently dabbled in it a bit and liked it quite a lot. I also really liked this article by Brett Cannon - Python core developer - on how it works under the hood.

PEP 465 - A dedicated infix operator for matrix multiplication

Matrix multiplication is a notably common operation in many fields of mathematics, science, engineering, and the addition of @ allows writing cleaner code.

Never needed to use it, but from what I understand it is a very helpful feature, especially to all people that use Python in scientific environments.

PEP 484 - Type Hints

Function annotation syntax has been a Python feature since version 3.0 (PEP 3107), however the semantics of annotations has been left undefined. While these annotations are available at runtime through the usual annotations attribute, no automatic type checking happens at runtime. Instead, it is assumed that a separate off-line type checker (e.g. mypy) will be used for on-demand source code analysis.

Another feature that I didn't have the chance to use. I did see it extensively used in Errbot source code though, and was intrigued by it since then.

collections.OrderedDict is now implemented in C, which makes it 4 to 100 times faster.

A small change, but I happen to use OrderedDict quite a lot so this is very nice to see.

Python 3.6 (not yet released)

PEP 519: Adding a file system path protocol

File system paths have historically been represented as str or bytes objects. This has led to people who write code which operate on file system paths to assume that such objects are only one of those two types. (...) To fix this situation, a new interface represented by os.PathLike has been defined. By implementing the fspath() method, an object signals that it represents a path.

You can't really escape from dealing with paths, whether in CLI tools or in web applications, so I'm looking forward to playing with this.

PEP 498: Formatted string literals

Formatted string literals are a new kind of string literal, prefixed with 'f'. They are similar to the format strings accepted by str.format(). They contain replacement fields surrounded by curly braces. The replacement fields are expressions, which are evaluated at run time, and then formatted using the format() protocol.

As small of feature as it is, I had mixed feelings about this for a long time. On one hand there are already too many ways to format strings which I don't like, especially in language that uses There should be one-- and preferably only one --obvious way to do it. as one of its principles.
On the other hand - I love this, as I usually use the same variable names and f'He said his name is {name}.' is much cleaner then 'He said his name is {name}.'.format(name=name) or 'He said his name is {}.'.format(name).


'Nuff said.

Python Developer Environment on OS X

I recently decided that my beloved ThinkPad x220 - the last of its kind before Lenovo started slowly destroying (changing? nah, let's go with destroying) the famous IBM computer line - was slowly getting too old. Don't get me wrong, it's an awesome computer, with perfect size, beautiful IPS screen (seriously, why are they so rare?), the best keyboard I ever had and TrackPoint that I will miss very much. And did I mention the built-in 3G module, docking station, and the ability to dismantle the whole thing with one screwdriver and half an hour of free time? Wait, what was I talking about?

Yeah, it was and still is an awesome computer. But 4 years is a lot, especially if it's also your work tool. All that came at the time when my brother was selling his old MacBook Pro which had me intrigued for a long time (after doing a bit of research and painfully removing ThinkPads from the consideration, the only real alternative to Apple was System76), so here we are.

After about a week of using it, I have a million thoughts, complaints, and pleasant surprises about OS X (or should I say "macOS"? ugh) and the laptop itself, but this is neither the place nor the time for those. Instead, I want to focus on setting up a Python developer environment on it.

As a long time Ubuntu and elementary OS user I kinda took apt-get and PPAs for granted. They're awesome. Need something? There's a good chance that it's already in the repositories or someone made a repository with it and hosted it (for free) on [Launchpad][launchpad], so it's available for anyone. So just do apt-get install planet-express and you're good to go, with automatic and centralized updates no less. Isn't that awesome? So... none of that on OS X.

Well, at least by default, because that's where the awesome Homebrew comes in. "The missing package manager for OS X" - rarely a description is so on point that I don't really have anything to add. Seriously, I don't think I would be able to use OS X without it. So install it if you haven't already:

/usr/bin/ruby -e "$(curl -fsSL"

Anyway, Python environment. The most basic setup would be brew install python python3, but I wouldn't really call that a Python developer environment. What if you need both Python 3.4 and 3.5? What about using tox and testing it under a number of Python versions? Virtual environments anyone? Homebrew doesn't really offer such flexibility (why would it? it's a package manager after all) and its maintainers agree - anything more complicated than using the latest version of Python 2 and/or 3 with it is a no no.

Luckily, pyenv comes to the rescue. It basically allows you to install any Python version released. You need Python 2.2.2 and 3.3.3 for some weird reason? Pyenv has your back:

$ brew install pyenv
$ pyenv install 3.5.2
$ pyenv install 3.4.5
$ pyenv install 3.3.3
$ pyenv install 2.7.12
$ pyenv install 2.2.2
$ pyenv versions
* system (set by /Users/bender/.pyenv/version)

You can then set these Python version(s) globally (rather self-explanatory) or locally (it saves passed Python versions in .python-version in current directory, so that it can 'set' them on entering it). Really cool stuff. But it gets even better.

Pyenv also has two plugins to manage so-called virtual environments (or just virtualenvs if you will) - pyenv-virtualenv and pyenv-virtualenvwrapper - both awesome looking, each with a slightly different approach. I previously used virtualenvwrapper and was tempted to do the same here. But after playing with both of them for a few minutes I opted in favor of pyenv-virtualenv, and I don't really have any complaints. Everything is straightforward and just works:

$ brew install pyenv-virtualenv
$ pyenv 3.3.3 flexo
$ pyenv virtualenvs
  3.3.3/envs/flexo (created from /home/bender/.pyenv/versions/3.3.3)
* flexo (created from /home/bender/.pyenv/versions/3.3.3)

The activation and deactivation also works as expected:

$ pyenv activate flexo
$ pyenv deactivate

I won't really go into the rest of my stack, but I can recommend the following:
- Homebrew Cask - Homebrew but for GUI apps
- Antigen - Awesome plugin manager for zsh, an alternative to oh-my-zsh
- iTerm 2 - Better terminal replacement
- Honukai Theme - Just a iTerm/zsh eyecandy, but what a eyecandy it is
- Sublime Text - Some prefer TextMate, I prefer Sublime Text
- PyCharm - My IDE of choice
- Dash - Extremely convenient offline documentation for programmers
- Tunnelblick - VPN client. Gets the job done

As always - feel free to ask any question.

Twitter Banner Switcher

We recently revamped our Twitter profile and connected it to both our blog and GitHub repos via the awesome IFTTT. Now every new blog post (like this one) and a release of one of our open source libraries gets immediately (well, check-every-15-minutes kind of immediately) tweeted about.

One thing that was missing (or more precisely: my boss thought was missing) was the ability to rotate over a set of images as a Twitter profile banner. And being programmers and stuff we did it (or more precisely: my boss told me to do it). It's called twitter-banner-switcher (I know - awesome name, right?) and is available now in stores near you (or more precisely: on PyPI and GitHub). I should stop with the more precisely stuff, shouldn't I? I should, or more precisely - I will.

Anyway - as always feel free to use, ask, fork, star, report bugs, fix them, suggest enhancements and point out any mistakes.

And have a nice day!

Ivona, Speak Dammit!

I don't know if you're familiar with Amazon's IVONA, but if you're not, let me just say that it's considered one of the best text-to-speech tools out there. Anyway, my boss needed an on-the-fly text to speech generator that makes use of IVONA's API for some project, and he tried to find a command-line script, but to his surprise, there wasn't anything that he liked. So he wrote one himself - in Go, for reasons unknown to me to this day.

But the fact that there wasn't anything like that made in Python didn't sit right with me. And who really uses Go for command-line tools anyway? Long story short - I made one as well. It's written (obviously) in Python and it's quite nice, if I do say so myself - for instance, it doesn't require you to explicitly provide all the attributes of the selected voice, while my bosses' version does. So there.

All you need to do to use it is to install it via $ pip3 install ivona_speak and then do something similar to like this:

$ ivona-speak --access-key 'ACCESS_KEY' --secret-key 'SECRET_KEY' / 
    -o good_news.mp3 'Good news everyone!'

Who needs mouths, am I right? They're so overrated anyway.

We (and by we, I mean my boss) also decided to factor out the part responsible for connecting to IVONA Speech Cloud into another library (as well as a GitHub repository). So, you can use python-ivona-api if you want to connect to IVONA from within your Python project, as we do in ivona-speak. How awesome is that?


How To Use Isso on Your Site

In our previous post, we covered why Isso is the way to go if you want comments on your website. We were specifically talking about Pelican, but this solution can be applied to other static site generators like Jekyll, Octopress, etc. as well. And for that matter, not just static site generators - Isso is universal and only requires the addition of a couple lines of HTML, so you could use it practically anywhere you wish. Now, let's help you get it working on your server.

I'll just go out on a limb and assume that if you managed to get your site up on the internet (getting a server, a domain, directing the domain to the server, installing some kind of webserver and configuring it) and you're running some kind of CMS or static site generator, then you have some technical knowledge. Or you're really smart. Or lucky. Or you're good at following tutorials on some random blogs (did I just burn myself?). Either way, I have to assume something and not start with 'What to look for when buying your first computer'.

And just to make things clear from the beginning - I will also be using apt-get as my package manager (if you use a different one, chances are you already know what to do) and Python 3 (to do my - however small - part in making the new version more popular).

Python knowledge isn't required, so don't worry if you never used it. What you need, though, is to have it - and a couple of other things - installed:

$ sudo apt-get install python3 python3-pip sqlite3 build-essential

Also, I wouldn't be myself if I didn't recommend using virtualenv, but it's not really necessary and I don't want to add yet another thing to learn for those of you that never used it and/or don't know what it is. So if you do know what it is, feel free to use it like with any other Python project, and if you don't then either read up on it (definitely worth it if you're interested in Python) or ignore this paragraph. OK? OK.

Enough small talk - let's install the damn thing!

$ sudo pip3 install isso

Wait - is that it? Well, kinda. If the whole thing was a one-liner then a 'How to' article on it wouldn't probably be needed, don't you think? We installed it, but we still have to run it in the background and enable it on the website.

In addition to that, before running it we have to create a config file. All options (like email notifications, comment moderation, and more) are listed in Isso docs, so go ahead and read those - I'll just include the ones that are required:

dbpath = /opt/isso/comments.db
host =

listen = http://localhost:8001/

They're pretty self-explanatory, but this is a 'How To' after all, dammit! So:

  • dbpath - path to SQLite database file; make sure it's writable by the user that Isso is running as
  • host - URL of your website (or websites, via docs)
  • listen - interface to listen on, that we will then proxy via our webserver

OK, let's assume that our config file is ready and we saved it as /opt/isso/isso.conf. Awesome. Now let's take care of running the Isso app in the background.

There is more than one way to do it -- I prefer supervisor, but my boss for example hates doing things nice and easy, so he made me use init (not even systemd! ugh) on our company server. So it's more or less up to you (or your boss...) to pick the method of your choice, with some example config files already provided.

And since I actually do like things nice and easy, let's move forward with supervisor:

$ sudo apt-get install supervisor

We then need to add the config file (/etc/supervisor/conf.d/isso.conf):

command = isso -c /opt/isso/isso.conf run
user = isso
autostart = true
autorestart = true
stdout_logfile = /opt/isso/supervisor.log
redirect_stderr = true
environment = LANG=en_US.UTF-8,LC_ALL=en_US.UTF-8

and, finally, run the thing:

$ sudo supervisorctl reread && sudo supervisorctl update
$ sudo supervisorctl start isso

(Now compare it to this atrocity.)

Afterwards, we can make sure that all is good in the world by checking if curl http://localhost:8001/js/embed.min.js returns some minified JavaScript gibberish.

As we established (and checked! -- remember kids, testing is important), Isso now runs in the background and is available at http://localhost:8001/. Let's make sure the webserver knows that. Again - I like Nginx, our company server is running Apache, so I'll cover both. The example in the Isso docs actually proxies it to a separate domain (like, but that seems like too much hassle for my taste, so we'll stick with a sub URI like

(Per my original assumptions, you managed to create a website, which means that you had to configure the webserver, right? Right? Anyway, that's what I'm still assuming.)

So all we need to do now is add one of the snippets below to our existing configuration:


location /isso {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Script-Name /isso;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass http://localhost:8001;


<Location "/isso">
    ProxyPass "http://localhost:8001"
    ProxyPassReverse "http://localhost:8001"

Restart it, cross your fingers and visit Recognize that minified JavaScript gibberish? If you actually do, then I'm a bit concerned - you shouldn't be able to recognize that. If you recognize it as some minified JavaScript gibberish, then everything's fine and you have only one step left - adding the HTML code to your template:

<script data-isso="//"

<section id="isso-thread"></section>

Even better - you can skip the last step if you for example use a Pelican theme that supports Isso, like let's say these two.

That's it. Yes, you made it to the end. Congrats. I hope it worked, or at least helped you in some way. If not then hire us and we'll do it for you for a small price of one million dollars. Or you can leave a comment and we'll try to help you. Either option is fine, really.

Static Sites and Comments. Wait, What?

Static sites don't really go together with comments. Because... how? Comments are dynamic by their nature. The whole point of a static site generator is to not have a web application running that renders content on request, and instead do the rendering/generating only when something's changed. Comments don't really fit into that logic. But, let's say you want them anyway. Or let's say your boss wants them on this very blog. (And you're thankful at least he doesn't want them in a modal window with animated graphics around.)

A little big of Googling, and it seems that all is good in the world - a static comments Pelican plugin, somebody already did what I'm trying to do. Awesome. The catch is that the approach is really... unique. The solution described in Static comments via email by Bernhard Scheirle, instead of adding a comment, sends you an email with the contents, that you then have to (semi-manually?) add to your repository and regenerate the site. Moderation is therefore built-in! :-) It's fully static, no doubt about it, but the more I thought about the idea, the less I liked it. Because to be considered usable in my (and, more importantly, my bosses) opinion, we would need to create a separate email inbox, and a parsing script that would read new emails, add them to the appropriate new comments subfolder and regenerate the site. Doable, but seems like more hassle then it should be.

Maybe Disqus then, a solution that's popular across the Internet, for both static and non-static sites? Yeah, that's what I proposed too, but my boss wouldn't allow it. No shady third party service shall have our data!, he said. They help advertisers target you based on the comments you leave!. Well, fair enough. Perl is a great programming language!, he added, and that's when I knew I don't stand a chance here. And after reading a couple of articles, he was kinda right (obviously I didn't tell him that). So let's find something else.

You would've thought that there are multiple options, especially for a fairly popular application like Pelican, but that's not really the case here. Pelican plugins repository has only Google Plus, Disqus, the already mentioned static comments via email solution, and a fully manual comment plugins. Is that it?

Fortunately, no. I took 'pelican' out of the equation and found Isso, which had me right from the start with the awesome xkcd comic on the front page. Written in Python, with SQLite as its simple database backend and integration via a couple lines of code. What's not to like? OK, it's not really static, but as we agreed comments by design can't be truly static. What it is though is really awesome. Did I mention the xkcd comic?

xkcd comic

(this one)

The documentation is plain and simple, and installation shouldn't be a problem, especially if you already managed to configure Pelican (or any other static site generator for that matter). Writing a straightforward configuration file, running the isso web app in the background, letting your webserver know where to look for it and adding the HTML/JS code is all you need to do.

With that, we've reached the end, and there's only one thing left to say: any comments?

My Brief Encounter with Daylight Saving Time

I never really cared about daylight saving time. Sure, I knew it existed, occasionally saw some articles making a case that it's not helpful in any way, but as long as my computer and phone knew when to switch - I was fine with it. One hour forward here, one hour back there, whatever. But I never really considered its practical implications, such as date management in software - like in this particular case.

A couple of weeks ago, a colleague of mine pointed out that when he copied events on our company instance of Schedoodle (which is our work schedule web app, built on Django) from week 21-27.03 onto the next one, some of them were off by one hour. I quickly found out that 27.03 is the magical day when daylight saving time change occurs, and 01:59:59 jumps to 03:00:00. It was obvious that this was the cause of the problem, so I had a starting point.

First, I looked at the code responsible for copying events from the previous week:

one_week = timedelta(weeks=1)

for prev_event in prev_week_events:
    start_datetime = prev_event.start_datetime + one_week
    end_datetime = prev_event.end_datetime + one_week


Nothing fancy - it just adds timedelta(weeks=1) to start and end datetimes. Hmm. Let's peek into the database - DateTimeField is smartly converted to UTC, and copying behaved as I thought it should - 2016-03-21 10:00:00 plus one week is 2016-03-28 10:00:00. Correct, isn't it? If only... Even though the timezone doesn't change, the hour offset for those dates does - 21.03.2016 is CET UTC+1h and 28.03.2016 is CEST UTC+2h. So when the datetimes are localized by Django, they get converted to 2016-03-21 11:00:00 and 2016-03-28 12:00:00 respectively. So I (kinda) know what's wrong but I still don't know which piece of code should be changed. When in doubt, Google it.

Both 'python daylight savings time' and 'django daylight savings time' point to a lot of similar Stack Overflow questions and blog posts but I would like to highlight those that helped me the most. First - and I should probably start with it both when the issue occurred and in this post - is Django's Docs about time zones. It's quite long, but mostly because the subject itself isn't trivial. It also points to pytz which is recommended by Django when more than basic timezone support is required.

But the key resource for me was Time Zones in Pytz & Django by Tommi Kaikkonen. I can't recommend it enough. It's quite lengthy but that's because it really exhausts the topic with multiple diagrams and code samples. It covers DST itself and then goes into explaining why it's such a buggy idea ending with examples of Python code with pytz and then how that all falls into Django itself. Also helpful - Troubles with timezones by Tikitu de Jager.

The essential part (for me) to understand from those links in this case was the difference between naive and aware datetimes and how that affects time addition operations with regards to DST and timezones. Both pytz and Django help a lot and have all the required pieces (have you seen django.utils.timezone?) - you just need to be, ekhm, aware of what you're working with and what you want to accomplish (I know, some shocking, eye-opening advice).

With all that newly acquired knowledge, I went back and reviewed all the code responsible for datetime manipulation. It wasn't bad (I was adding tzinfo=timezone.get_current_timezone to datetime on creation instead of using timezone.make_aware()), but it definitely could be made better - and now it is. As to the problem itself - understanding it was the main part. Most importantly, always operate on UTC datetimes and work it up from there. With that in mind, all it took were just a few simple changes:

one_week = timedelta(weeks=1)

for prev_event in prev_week_events:
    # Yay daylight savings time!
    tz = timezone.get_current_timezone()
    start_datetime = tz.localize(
        timezone.make_naive(prev_event.start_datetime) + one_week
    end_datetime = tz.localize(
        timezone.make_naive(prev_event.end_datetime) + one_week


Well, at least now I do have an opinion about daylight saving time. I don't like it.