<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Olaf Górski</title>
    <description>The latest articles on DEV Community by Olaf Górski (@grski).</description>
    <link>https://dev.to/grski</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F951129%2Fe7e29cfc-b9c7-40d4-acab-82b1316dd6fe.png</url>
      <title>DEV Community: Olaf Górski</title>
      <link>https://dev.to/grski</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/grski"/>
    <language>en</language>
    <item>
      <title>Why your brain is 3 million times more efficient than GPT-4 - introduction to Vector Databases and their comparison</title>
      <dc:creator>Olaf Górski</dc:creator>
      <pubDate>Sun, 23 Jul 2023 07:28:02 +0000</pubDate>
      <link>https://dev.to/grski/why-your-brain-is-3-million-times-more-efficient-than-gpt-4-introduction-to-vector-databases-and-their-comparison-593g</link>
      <guid>https://dev.to/grski/why-your-brain-is-3-million-times-more-efficient-than-gpt-4-introduction-to-vector-databases-and-their-comparison-593g</guid>
      <description>&lt;p&gt;Recently I had to go on journey into the Vector Database world and pick one for a particular project. And oh boy, was it a ride. Recently Vector Databases have gained a newfound attraction and spotlight thanks to the rise of LLMs. Such situation is both a blessing and a little bit of a curse, bringing some of the things attributed to LLMs to Vector Databases too - the perception they are 'a new thing', untested technology or some shiny crap good only for LLM-based stuff. All of which is wrong.&lt;/p&gt;

&lt;p&gt;Vector Databases have been with us for waaay longer than most know. There's legit engineering, science and rigorous testing behind most of them over the span of multiple years. And there's a lot of them, they all have their own quirks, just a little bit like RDBMSes do, except the situation there is quite clear, at least for me, in most cases postgres FTW, I mean it's not 2005 anymore, we won't go with mysql or LAMP stack, right? Do most of you even remember what it is or am I exaggerating things?&lt;/p&gt;

&lt;p&gt;In case of Vector Databases I was, and still am, actually, completely green. So I had to go around, gather facts, check stuff out and form my opinion regarding each particular database, as to select one for my particular use case.&lt;/p&gt;

&lt;p&gt;Before we begin, let me put a disclaimer here. I do not title myself as an expert in AI. There are all my just personal ramblings, opinions, certainly biased. Take everything you read with a grain of salt and do your own due dilligence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction to Vectors, Similarity and our amazing brains
&lt;/h2&gt;

&lt;p&gt;Before we actually begin I think we should dig a bit deeper into what exactly we are playing around. If you are not interested in understanding how this all works in detail and how it relates to generative AI, feel free to skip this part and go over to the next section.&lt;/p&gt;

&lt;p&gt;First we must lay down certain axioms (smart word for the common sense/ground rules we all agree upon and accept as true). &lt;/p&gt;

&lt;p&gt;One of such would be the fact that currently computers do not really understand words. They operate in what is called binary language, so a string of 0s and 1s. It's related to how electric charge, electrons and atoms work. Physics stuff. Very basics of CS and electronics.&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Computers do not understand words, they operate on binary language, which is just 1s and 0s, so numbers. Computers only understand numbers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Knowing that, it becomes obvious, that we may face some problems. First of all if it's all just 1 or 0, how do we express different numbers? Well, that part is easy, because thanks to the decimal counting system. Long story short, we have 10 digits to express stuff, in binary you have 2. That means that you still can represent other numbers with base 2, the resulting number will be just a bit longer and calculated differently. I won't to into too much of a detail here, you can look it up yourself -&amp;gt; binary system, decimal system and how to convert from one to another. Or ask chatgpt for an explanation. It does a fine job. Just know that any natural number can be easily converted from base ten to binary e.g. "4" in decimal i s "100" in binary. They hold equivalent value, but are expressed differently. The case is a bit more complicated for floating point numbers, but let's not get into that now.&lt;/p&gt;

&lt;p&gt;So first layer of abstraction is down. We have computers. Electric. Electrons. High current/low current, current/no current. 1s and 0s. &lt;/p&gt;

&lt;p&gt;These 1s and 0s of ours now can magically become numbers. This is what our computers operate on - in every case. When you dig deep enough, everything you interact with in the virtual world is in fact a number, so a particular configuration of these tiny little something's in the computer that either have electricity running through them, or they don't and that is interpreted as a number. Imagine 3 light bulbs. 1st is on, 2nd and 3rd are off. 1-0-0. We magically agreed that this means 4. Now make the size smaller probably by a thousand, a milion times or something, and you get an idea what's going on in that shiny macbook of yours. Back to the topic.&lt;/p&gt;

&lt;p&gt;Numbers. Yes, we have then and are able to somehow interpret the current state of your computer, or it's part as it being a particular number.&lt;/p&gt;

&lt;p&gt;Now that's not very useful, right? We, humans, mostly operate on text and language, right? Yup. Imagine a world where you have to decode numbers. in some magic dictionary to a word. The computers wouldn't be so useful now, would they? But that's how it looks in the background. Long time ago some smart dudes have gathered around that agreed that from now on, if we know we are dealing with text, we should interpret the number 65 as "A", "B" as 66 and so on. Of course, it changed, different people had different standards and agreements but who cares, let's simplify. We come to understand the second axiom:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We, as socity, have assigned certain roles/meaning to particular numbers, in this case it's letters.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now that we have our computer, that understands numbers, and we know which numbers in which case are which letters, we kinda can start forming words for example. And if you have words you can have sentences, code, whatnot. That's more useful. All of your software builds on top of this base truth.&lt;/p&gt;

&lt;p&gt;Now that we know that all words in fact, are nothing but numbers for your computer, we can go further than that.&lt;/p&gt;

&lt;p&gt;For the computer, a given word, regardless of the context, based on the above, holds the same 'meaning' or 'value'. So even though you can say 'dust the furniture' as in clean or 'dust on the furniture' as in there's a mess, the representation (and the meaning) for the computer would stay the same, even though there's a mess and clean are two totally different things. So, even though we can convert a word for a distinctive number for the computer so it 'knows' kind of, what's up, there's missing 'context' to judge the meaning.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Words simply represented as binary numbers lack context.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Well, humans can be ingenious in the ways they do wicked things, but also in the ways the do great things. One such example here would be Contextualized word embeddings. What does that stand for? Well, let me tell you. &lt;/p&gt;

&lt;p&gt;So we have this now simple case of turning a series of 1s and 0s into numbers, numbers into letters and then words. That you already understood, right? We kinda explained the problem with that - lack of context. Well. We have developed methods that allow us to generate different and unique numbers for words, depending on their context or semantic meaning. So if we used Contextualized Word Embeddings in our example above, the 'dust' from 'dust the furniture' would get totally different number than the word dust in 'dust on the floor'. Now the computer knows that these two have different meaning! How does that exactly work? It's a piece of work, let me tell you, but we won't cover that in this text. First of all I'd have to understand that myself haha. &lt;/p&gt;

&lt;p&gt;Long story short, we have a magic function that does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;number_representation_of_a_word_and_meaning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dust"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"dust on the floor"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="n"&gt;number_representation_of_a_word_and_meaning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dust"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"dust the floor"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The values are not random - words that within certain context have different meaning probably will have very different number assigned to them. A number that is far apart from the other one. If the meanings were similar, but just slightly different depending on the context, we'd have numbers that are closer to each other.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;number_representation_of_a_word_and_meaning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bark"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bark is a noun"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
&lt;span class="n"&gt;number_representation_of_a_word_and_meaning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bark"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bark is a verb"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why is that? "Bark" and "bark" both relate to something that is assosciated with outer protection or covering. If a dog barks usually it is a sign of danger, something related to safety, protection. The tree has a bark in order to protect itself from the environment. It's different, still, but this time we share a certain common theme at least in some sense, hence the numbers we got are closer together than in the previous case, because they are more similar.&lt;/p&gt;

&lt;p&gt;Also&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Similar meaning of words, in a particular context, converted to numbers, are closer to each other than radically different meanings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By the way, what are embeddings you might wonder? Why did I not describe it earlier? Because we need to understand the above to understand embeddings. More or less it's what we described, so the way of representing a number based on particular set of criteria (in our case the semantic meaning) as similar numbers, even if they are not similar raw value wise. So in the embeddings world, 'dust' would be, as a number, closer to 'clean' than 'dust' would be to 'dirt'. At least that's how I understand it. And 'dust' as in 'dust the floor' would be far away from both 'dust on the floor' or 'dirt'.&lt;/p&gt;

&lt;p&gt;Alright, this one was a bit trickier, but we got there eventually. &lt;/p&gt;

&lt;p&gt;Again, let me reiterate. We have electrons and then electricity. Because of electricity (or to be more exact high/low current) we have 1s and 0s, so binary language. Next are numbers, binary numbers, so just numbers but expressed only with 1s and 0s, base 2. That gets converted to base 10, so the decimal system that we humans mostly use (not everywhere and not always lol). These numbers then get simply mapped to words with the same values regardless of the context, of the meaning, in the base case.&lt;/p&gt;

&lt;p&gt;But nowadays, we iterated over that and can assosciate words with their meaning and assign different numbers, based on the context of the text or a sentence (sentence transformers sounds familiar?) and their semantic meaning. The groundbreaking publication about Transformer model. Go look it up. By the way look at the year it was published and how quickly we went from theoreticall stuff to something as amazing as GPT-4. INSANE. Either way. Semantic meaning, embeddings and transformer model. This is the reason that the LLMs or generative AI can sound so convincing, as if it really knew the stuff it generated. Nope, it doesn't. It just processes numbers, they do not intrinsically understand the meaning, they understand that 1 is closer to 2 than 24 is to 42, which is the answer to everything. See what I did there? This does not imply it 'understands' the meaning in the human sense and why I'm quite sure that AGI (Artificial General Intelligence), so something that 'really' thinks in the most human way, is so far away. The current technology and models, simply do not work like we assume them to.&lt;/p&gt;

&lt;p&gt;Okay champ. We have these words mapped to different numbers, which are either closer or further away, depending on the semantic meaning in a particular context. What now?&lt;/p&gt;

&lt;p&gt;Well, math magic comes in. I ain't no maths guy, I do enjoy the thought exercises or elementary and truthfully representation of stuff it can provide, nowadays I'd struggle to solve quadratic equation probably, being just a simple highschool dropout, but let me share with you my understanding of this absolutely marevelous phenomenon that happens.&lt;/p&gt;

&lt;p&gt;So, numbers. In maths we have this thing called Algebra. It's a study of relationships between things that vary over time, which are represented by symbols, usually letters, which also can represent things, usually numbers. What? ... Yeah. That would be my reaction to this statement too, at least was in the past. Let's make it a bit more digestible. To put it in simple terms, for us numbers are things that allow us to measure reality, count things, they are building blocks of reality you could say. Almost everything (or as some would argue - everything), given a proper formula, could be represented as a number. Be it something so physical as the length of your arm, number of apples in your kitchen or something as abstract as human language or even the meaning of a word, which we have demonstrated above. That however relates to this particular branch of mathemathics and 'real' stuff. In here numbers are immanent, simple, natural, the best. What about branches of maths that operate on a bit different set of rules and so on? Different universes of sorts? Well, in there numbers might not be the best base unit of operation, in different universes we might need either different base building blocks or we might want to take numbers and extend them a bit, if you will, to make life easier. &lt;/p&gt;

&lt;p&gt;One such example is the study of Linear Algebra. Linear Algebra has 'vectors' (which in fact are numbers with certain additional stuff to abstract ideas and make life easier) the same way basic Algebra has numbers/symbols/variables. I won't go into the details here too much as my understanding is also not so perfect, and probably I'd spout some gibberish, but I hope you get the analogy.&lt;/p&gt;

&lt;p&gt;So in Linear Algebra we have vector spaces, which deal with vectors, that have something to do with 'space' in general. Space might sound familiar because we talked about it. Remember the 'distance' between the numbers which were representations of meaning of a given word in a particular context? Yep, the same stuff let's say. &lt;/p&gt;

&lt;p&gt;Space can have many 'dimensions', similarly with vectors. If you have one dimension, it's just [1]. If we have two, e.g. X &amp;amp; Y axis, we can position a point in place by having, well, two numbers for each dimension, eg [1,1], this should ring a bell if you remember anything from your middleschool math class.&lt;br&gt;
What if we have 3D space? Well, similar stuff. Notice how each additional dimension multiplies the number of all the possible vectors. So let's assume we are working on a limited range of natural numbers, 10. In 1D case, we have 10 options. In 2D space we have 10 possibilities for first one, 10 for the second one, which totals to 10 * 10 = 100 possible different combinations. In case of 3d? You get the idea.&lt;/p&gt;

&lt;p&gt;So now imagine how insanely big the possibilities are, if we are working with a bit of a broader range (e.g. 65536 or 256) and not with 3 dimensions, but with 1536! That's INSANE. Absolutely insane, but also what allows our LLMs to seemingly perceive so many nuances in the meanings of the words and what nots. &lt;/p&gt;

&lt;p&gt;I know it was a long digression, but bear with me. Again. We have electrons and then electricity. Yeah, I'll spare you that repetition again. Number representation of a word based on semantic meaning. So we have a number per meaning in a given context let's say. This number gets then turned into a vector which lives inside vector space. To accomodate for the many meanings and contexts and how contextual our actions, words and speech are, this vector space should have appropriate number of dimensions. It can't have too much as it'd get too huge to process. OpenAI settled for 1536 dimensions. Remember each 'dimension' can be assigned a different 'value', so the total number of possible meanings is THE_MAX_NUMBER_WE_OPERATE_ON to the power of 1536. THAT"S A LOT. Anyhow.&lt;/p&gt;

&lt;p&gt;We went from having 1s and 0s, to our words being evaluated in 1536 dimensions or 'meanings'. The more true to a particular meaning is that word, the higher the value it'll get in that dimension.&lt;/p&gt;

&lt;p&gt;As you can see we have a problem now. How do we actually process such a humongous amount of data? Traditional methods don't really work well or would be too expensive computationally or economically. This is what we call "curse of dimensionality". To deal with this stuff like Approximate Nearest Neighbour Search problem show up. Meaning: how do we find similar vectors (numbers) in very high dimensional spaces? &lt;/p&gt;

&lt;p&gt;To resolve that puzzle we came up with Hierarchical Navigable Small World or HNSW. What is that? Long story short, even though the space is so HUGE, usually, in large networks, eg. human ones or social ones, despite the size, most nodes can be reached from any place in the network with surprisingly few steps. It's often reffered to as "six phones rule", that states that you are at maximum six phone call aways from anyone in the world. You know someone who knows someone and so on, then boom. Your message gets delivered to Obama. It's quite interesting actually. Important to note is that not all nodes in the network are connected equally. There are these things called as hyperconnectors, which connect to A LOT of nodes, and the contrary so, the nodes that are a bit lonely. I think you get the idea and seen an example in your life - almost everybody knows that someone who seems to know everybody everywhere. Either way, as I digress. If we take this Small World thing and add one more thing on top of it - hierarchy, managing even such a huge amount of data becomes doable. What does it look like? Simple. Think in terms of enterprise org chart. It's as if, let's say, you were a CEO and wanted to know something about if your company does use type hinting in python like human beings do. In HNSW the approach would be to first select a candidate, who might know something (be similar to the topic/value you search for) from a very few selected people in the company (mby the hyperconnectors should be at the top? ;)). Let's say the are Lead/Director level people. So you e.g. have Director of PeopleOPs, Director of Product, Director of Engineering and so on. Out of these, the semantic value of Director of Engineering will be most similar to "do we use type hinting in python". So we go to him and there we repeat the process. However our guys here are lazy for whatever reason. he doesn't want to check or write the answer, so he delegates.  -&amp;gt; who does the director know, who in the hierarchy is the level below him (these are the guys he knows best, he doesn't know the ones lower too well as he doesn't interact with them often) that should know the answer? So he repeats the process: Engineering Manager in Frontend team, EM in DevOps, EM in Backend team. Backend Team it is.&lt;/p&gt;

&lt;p&gt;Now this process gets repeted till we hit the bottom most layer of hierarchy, how many there are is up to the organisation to decide.&lt;/p&gt;

&lt;p&gt;This way instead of the CEO asking maximum number of N people in the worst case, assuming that only the last person knew, the question gets asked only N maximum number of times, where N is the number of layers in the hierarchy we have. Or something like that.&lt;/p&gt;

&lt;p&gt;Of course if he asked the question to everyone he would have better data and could select the best answer. The one he will get in the case of using HNSW will be good enough (how good we want will require us to define a certain metric, but the more accurate the answer needs to be, the tricker it is computationally) but will cost SIGNIFICANTLY less to get. Think sth like rolling out a feature that covers 80% of the needs in 2 weeks vs perfecting it to 99.99% in 2 years. 80% (or 50 or 95) is usually good enough when you weight the pros and cons, cost/benefit.&lt;/p&gt;

&lt;p&gt;WOAH. Long story long, this is what enables us to process such huge amounts of data taht is so high in dimensionality, that can accomodate the various context, nuances and meanings that is assosciated with for example human speech or thinking process.&lt;/p&gt;

&lt;p&gt;Now imagine our brains do this at a higher and more profund level than this, they do it constantly, all the time, they fit into something small like our skull and run on the equivalent of 24 Watts of power per hour. In comparison GPT-4 hardware requires SWATHES of data-centre space and an estimated 7.5 MW per hour.  So around &lt;strong&gt;312 500x&lt;/strong&gt; less while doing stuff that is thousand times more sophisticated. WHAT THE FLYING FK. We are a wonder of Nature. An amazing, fu**ing mess of a wonder but a wonder nonetheless. Also, keep in mind that our brains don't dedicate 100% of the horsepower to conscious thinking processes. According to Auburn Unviersity paper I found, cognitive neuroscientists believe that only 5% of our cognitive activity is conscious. So multiply this number by anything from 20 to a 100. Let's be conservative and say 10 for some reason. That's still over 3 million times the efficiency than GPT-4, while also having at least a magnitude (or at least N) greater complexity. I got carried away in the amazement. &lt;/p&gt;

&lt;p&gt;TLDR: &lt;br&gt;
Electrons -&amp;gt; Electricity -&amp;gt; Binary -&amp;gt; Binary Numbers -&amp;gt; "Human" or base 10 numbers -&amp;gt; Letters -&amp;gt; Words -&amp;gt; Embeddings -&amp;gt; (Semantically) Contextual Word Embeddings -&amp;gt;  Vectors -&amp;gt; Vector Spaces -&amp;gt; High Dimension Vector Spaces -&amp;gt; Approximate Nearest Neighbour Search or just Nearest Neighbour Search (ANNS/NNS) -&amp;gt; Hierarchical Navigable Small World (HNSW)&lt;/p&gt;

&lt;p&gt;Soak up all these terms and write them down.&lt;/p&gt;

&lt;p&gt;From here we are almost done. If you've been following closely you might start to piece the puzzles together.&lt;/p&gt;

&lt;p&gt;LLMs are in fact just algorithms, very complex ones, that have indexed or ingested A LOT of data/words, vectorized and embedded them and their meanings in particular context. Then, based on that data, they just 'guess', based on context, that if we have this particular thing or meaning in a semantic context, so embeddings, so nubmers or vectors in high dimensional space, just right here, we probably have in mind this particular response which is nothing but a set of other embeddings. &lt;/p&gt;

&lt;p&gt;Also usually when we speak about embeddings, we deal with tokens, which usually are not whole words but like ~4 characters in English. &lt;/p&gt;

&lt;p&gt;So if based on the context and approximation, we know that this particular query seems like something that will be assosciated with this particular token, we will use it. Then if we have one token ahead, we can repeat the process till we start hitting lower levels of assosciation, which in turn means that we can probably stop generating as the answer is complete.&lt;/p&gt;

&lt;p&gt;So... Yup. There's no thinking happening. Just assosciating numbers and guessing. You read that right. GPT-4 does not 'think' at all. It's as far away from human consciousness as we are from Putin being a good guy. This simply does not come even close. So it's the reason I do not worry at night (at least yet) AGI will come and replace me or go on a consciouss crusade against humanity.&lt;/p&gt;

&lt;p&gt;Bear in mind, my understanding of the stuff is VERY limited, I'm all new to this. I've simplified A LOT not only for the reader, but also for myself. If any of this is unjustly inaccurate, let me know. I've googled most of the stuff along the way. &lt;/p&gt;

&lt;p&gt;WHOAH. DAMN THAT WAS LONG. &lt;/p&gt;

&lt;p&gt;Let's answer the initial question then. How do vector databases work then? Well, they take the text. Vectorize it. Embedd in high-dimension space, then if you query it, they search for approximate similar tokens/pieces/texts. Simple, right?&lt;/p&gt;

&lt;p&gt;Now with that over, let's get to the stuff I had in mind when starting the article, so comparison of vector databases and a raport from using some of the more popular ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting point - chromaDB
&lt;/h2&gt;

&lt;p&gt;My starting point was &lt;a href="https://www.trychroma.com/"&gt;chromaDB&lt;/a&gt;. It allowed me get up &amp;amp; running quickly and seamlessly. It was dead simple and it got the job done. However it wasn't the best approach TBH. For a PoC? Yup, it did get the job done. But for more serious, potentially production-grade stuff? Not given how I implemented it. &lt;/p&gt;

&lt;p&gt;Long story short, for a PoC, I created this Q&amp;amp;A over a scraped knowledge centre app let's say. I needed a VectorDB deseprately. Pinecone was off-record for obious reasons that I'll list later on in the article, even though it was the easiest to implement I'd say. There was stuff like FAISS and what not, but the choice, over initial exploration, fell on chromaDB. &lt;/p&gt;

&lt;p&gt;I used a very dirty approach, where I packaged it together in one container with the API backend part of the application. ChromaDB persisted the vectors in their internal format in a certain directory in my project's repo. I simply commited it to the repo. Not the best, I know. &lt;/p&gt;

&lt;p&gt;Now, the whole VDB (Vector Database) was a part of our repo. I locally ingested the data that I needed in the VDB, then commited the state of the VDB to the repo.&lt;/p&gt;

&lt;p&gt;chromadb got added as a project dependency (installing it is quite trivial - it's just a python package) to pyproject.toml and boom. Initialise it somewhere in the code during application startup and boom, you done. Again, this is very dirty approach. &lt;/p&gt;

&lt;p&gt;API is packaged in the same container as the DB. In this setup changes in the VDB  are not really persisted unless you commit them to the repo  - only the commited state matters (so read-only apps or go home champ). They share resources. If one fails, the other does too. It was somehow scalable given that we had read-only app, so it was self-contained but... &lt;/p&gt;

&lt;p&gt;The git repo got big, fast. The docker image build time got very long for my standards. I mean imagine 25-30m build time for a simple fastAPI app. Not exactly desirable. This came from the fact that the chromadb was inside the container and had to be rebuilt each time the code changed, and as you can imagine, rebuilding db package can be quite time consuming (looking at you, psycopg).&lt;/p&gt;

&lt;p&gt;We couldn't go on for long with this approach. Ofc you can run chromadb as a separate container, self-hosted, but that added overhead I didn't need at that time for a PoC. At that time, from what I remember, there was also no option to do proper clustering that'd allow you to get prod-level scalability, survavibality and so on. TLDR: you could run with a single instance and that's it. I also didn't see helm/k8s OOB support that'd make it easy to deploy in a cluster somewhere. On top of that other projects were in the pipeline, then what? Each of them should have their own instance? How would we share data? All of this meant, it had to go away.&lt;/p&gt;

&lt;p&gt;But it allowed me to get off the ground, integrate fast, LEARN a lot and so on. For this purpose it did an AMAZING job.&lt;/p&gt;

&lt;p&gt;Also, what needs to be said is &lt;strong&gt;that some of the features mentioned weren't supported at the time&lt;/strong&gt; I was creating the project, which was &lt;strong&gt;couple of months ago&lt;/strong&gt;. Since then &lt;strong&gt;Chroma has closed $18M seed round,&lt;/strong&gt; developed a solid roadmap and started addressing some of what was mentioned here. We had a nice talk with Jeff Huber, chromadb's Founder, about all of this. I really loved a lot how he was humble and helpful enough to admit, that &lt;strong&gt;given the preconditions for the project &amp;amp; productionisation&lt;/strong&gt;, chroma &lt;strong&gt;at that time&lt;/strong&gt; wasn't exactly the best choice, and choosing something else for our needs was a smart move! He is a &lt;strong&gt;very intelligent, passionate and brilliant guy,&lt;/strong&gt; so I'm more than eager to see how they develop, &lt;strong&gt;however till we see the features shipped&lt;/strong&gt;, in the use cases we had in mind, it was a no-go. However to get started, have one-self-contained app with db inside and be done in 4h instead of 4 days (I know i exaggurate)? Sure, why not. If I was to make a comparison to something people are more familiar with,RDMBSes, I'd say the current chroma is like sqlite. Easier to comprehend now where it can shine and where it won't? Great. Also remember, my approach was simple. If you do it properly, set up all the proper stuff, configure it nicely, adhere to best practices, you probably won't run into any problems. I did not do that, lack of time &amp;amp; expertise.&lt;/p&gt;

&lt;p&gt;A short edit here needed to happen. I've reviewed this together with Jeff (THANKS) and he shed light on how most of these issues got addresed in v0.4. &lt;/p&gt;

&lt;p&gt;They made an architectural shift in their design, changed the storage engine, the build is now at least 20x faster (which now makes my point invalid), requires way less memory (that was not an issue though, but is good to see) and is far more performand and durable (even in April I did not run into any issues with durability). Overall, as I've predicted before actually getting the message from Jeff, it's been a short time, yet they've made amazing progress. This made me reshuffle the recomendations part a bit and this + the coming changes, make chroma a strong candidate to evaluate. I still need to play around with it more to see the exact progress but seems promising.&lt;/p&gt;

&lt;p&gt;So now, most of the stuff that I mentioned in this section, that were relevant at the time of making the choice - April, are not a concern now. Keep this in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;So if chromaDB was out of the picture. What next? Let's see what's out there.&lt;/p&gt;

&lt;p&gt;Let's start with stuff that failed fairly quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pinecone
&lt;/h3&gt;

&lt;p&gt;Long story short. It's a black box hosted somewhere, VDB as a Service. Proprietary. No-go. I want control over my data, where my db is deployed, where it lives and so on. Sorry. Privacy &amp;amp; Compliance would not be happy, same with myself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Faiss
&lt;/h3&gt;

&lt;p&gt;It's just a library. not a full fledged VDB. It'd be a step back compared to chroma, so no. But for tiny PoCs? Possible. Evalute it. I didn't do that in detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Milvus
&lt;/h3&gt;

&lt;p&gt;This one seemed promising. Initially I thought we'd roll with them. Promised all the new shiny nice stuff, but then I actually started using it. Had troubles to even index the data. Started running into bugs (or documentation not explaining stuff dumb enough for me to get it or not explaining at all), weird edge cases and complications that I was not in for. In case of chroma I just plugged it in, ran my Langchain-based data-ingestion service and I was done. Here? Forget it. However it was farily decent and pleasant to run locally in docker, a bit less so in k8s, but still passable. Bonus points for OS, self-hosted version and on-prem + cloud possibilty. If I was to make a similar comaprison as I did in case of chromadb, I'd say Milvus is the old Oracle DB of Vector Database world. Plus I've heard some similar opinions and unconfirmed rumours regarding ceratin dubious possible origins/practices and policies. It did seem promising but ended up as  a failure last minute.&lt;/p&gt;

&lt;p&gt;Eventually I did succeed in ingesting the data and so on, but the results were not satisfactory. Performance was okay, but I was done with frustration.&lt;/p&gt;

&lt;h3&gt;
  
  
  pgvector
&lt;/h3&gt;

&lt;p&gt;This one disappointed me greatly.&lt;/p&gt;

&lt;p&gt;Bad performance. Unusual selection of the underlying algorithm. Some problems with accuracy, especially when concurrency comes into play. Think carefully if the ease of integration is worth it. In my opinion this + the performance weren't. Take a look at the benchmarks. It's not satisfactory to say the least.&lt;/p&gt;

&lt;p&gt;It also turned out that it's not supported everywhere as it's relatively new extension, for postgres standards. &lt;/p&gt;

&lt;p&gt;And it's me who says this, guy who is team postgres. But postgres is a great RDBMS. Again, it does not specialise in vectors. &lt;/p&gt;

&lt;p&gt;The benefit here is it being a standard piece of infrastructure, having your data in one place, all the other benefits postgres brings. It's still useful piece of software.&lt;/p&gt;

&lt;p&gt;Bear in mind though, that in AWS world, aurora does not support it yet. Meaning if you are in aurora world, you'll need to deploy a separate base postgres instance with pgvector enabled. This eliminates the main benefit.&lt;/p&gt;

&lt;p&gt;However if you do not care about that, you are running your own pgvector instance, and just want to get started? Hm, maybe. So there might be some relevant and valid use cases, but not in my case.&lt;/p&gt;

&lt;p&gt;The performance was not there, accuracy also, especially when concurrency is considered and throughput. We couldn't deploy it in our Aurora cluster, we'd need to go for separate RDS. I think I'll pass, however it was indeed tempting because of the postgres brand, it being so reliable and common, but let's consider the borader picture - this was 2nd iteration where I expected things to be done in a proper, future-proof manner.&lt;/p&gt;

&lt;p&gt;For RDBMS just go postgres. For VDB? Think twice about using pgvector.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis
&lt;/h3&gt;

&lt;p&gt;Redis was a strong possible candidate. Good performance. Standard piece of infrastructure. Everyone knows redis, right? Features on the thin side though. I wasn't sure if, given some time, it'd be enough. But I kept it in place. It more or less worked. However It lingered in my mind that Redis is not specialised in Vectors. It's key value store. This is not the core of their business. Sure it'd be nice to be able to just deploy redis and have it done with. Everyone knows redis. However doubts sprouted in my mind, rightfully so eg. given their recent sunsetting of Redis Graph. &lt;/p&gt;

&lt;p&gt;However if you already have a Redis cluster running somewhere, for starting it might be enough. &lt;/p&gt;

&lt;p&gt;There is the question of perfromance, sure, but it's acceptable.&lt;/p&gt;

&lt;p&gt;So, yeah, viable choice, but make a conscious decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Qdrant
&lt;/h3&gt;

&lt;p&gt;Open source? Check. Self-hosted option? Check. Cloud if you are lazy? Check. K8s? Check. Clustering? Check. Survavibality? Check. Performance? Off the charts. Community? Great. Integration with langchain and other libs? Yup. One-line docker and you good to go locally? You bet. I was sold. &lt;/p&gt;

&lt;p&gt;But then it got better. Within hours of signing up for their cloud offering to test things out I was approached by the founders to check in. Big shotout to &lt;a class="mentioned-user" href="https://dev.to/andre"&gt;@andre&lt;/a&gt; Zayarni, &lt;a class="mentioned-user" href="https://dev.to/fabrizio"&gt;@fabrizio&lt;/a&gt; Schmidt and &lt;a class="mentioned-user" href="https://dev.to/andrey"&gt;@andrey&lt;/a&gt; Vasnetsov. Keep doing what you doing. We had a meeting set up right after. Tremendous knowledge. I really like the spirit they operate in or the values they highlight in some of their blog articles, especially the one related to closing the seed round. Andre's writing is sharp AF. Snarky remarks about the article not being written by chatGPT get bonus points (btw gonna steal that one).&lt;/p&gt;

&lt;p&gt;They offered help (despite me not earning them a dime at that moment and stating that it'd be the case for the coming future or it'll be dime a dollar) with everything. Accommodated to our needs. Shared slack channel if problems arise? There you go. You wanna learn more? Sure, here are the resources. Workshops? Possible.&lt;/p&gt;

&lt;p&gt;Also, big shotout to &lt;a class="mentioned-user" href="https://dev.to/kacper"&gt;@kacper&lt;/a&gt; Łukawski, which is just in love with spreading knowledge and helping out people. He provided lots of insights and offered help to get started out. Real beneficial stuff. Tutorials, blog posts, integrations with most popular libraries.&lt;/p&gt;

&lt;p&gt;Free tier in the cloud offering to test stuff out (that actually can take you far along?) provided to anyone. &lt;/p&gt;

&lt;p&gt;Qdrant wins by far, in my experience, when it comes to performance, scalability, durability, ease of use, feature set, flexibility and most importantly community plus the company values.&lt;/p&gt;

&lt;p&gt;On top of that you can get started in minutes. It was best performance, easiest to use &amp;amp; set up, well documented, nice community. Clear win.&lt;/p&gt;

&lt;p&gt;As you can see I'm totally biased and sold, so take what I wrote with a grain of salt and verify yourself all of the above. I'll however remain quite bullish &lt;/p&gt;

&lt;h3&gt;
  
  
  Weaviate
&lt;/h3&gt;

&lt;p&gt;Heard some good stuff here, especially regarding the feature set. However did not try it out personally. Compared to qdrant tho, they do have to improve on performance probably, but it might be a valid potential choice. Plus their community seems nice. Or I'm being biased only because &lt;a class="mentioned-user" href="https://dev.to/philip"&gt;@philip&lt;/a&gt; Vollet, so their head of Dev Growth laughed at my joke about pgvector, so he seems salty in similar way to myself, which implicates good stuff. Eh, Olaf and his shenanigans again. Either way. Check it out, should be ok.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Qdrant rocks and wins. In all categories. It's postgres of the VDB world.&lt;/li&gt;
&lt;li&gt;Weaviate seems to be nice too, but don't quote me on that. Features seem nice and rich for LLMs.&lt;/li&gt;
&lt;li&gt;Pinecone gets you started in minutes, but so does qdrant and why would you lock yourself in their ecosystem? Plus the performance and price, but might be valid choice for some folks just coz of the Managed mindset and convenience.&lt;/li&gt;
&lt;li&gt;Chromadb lacks certain features, but develops quickly, however it needs evaluation and observation for bigger projects or situations where we need real clusters. For smaller ones with proper setup I'd say it's a valid choice + look out for their development and upcoming features. A bit like sqlite of the VDB world, but on steroids currently.&lt;/li&gt;
&lt;li&gt;Redis is acceptable, but doesn't shine IMO. Positive surprise performance wise. It's not core of their business tho, plus they seem to be sunsetting certain parts that do not belong to the core like RedisGraph.&lt;/li&gt;
&lt;li&gt;pgvector is disappointing, but still can be valid in some use cases and scenarios, it levarage the postgres brand and benefits which also enforce certain limitations on it. Do not count on great performance though or accuracy with concurrency.&lt;/li&gt;
&lt;li&gt;Milvus is what I'd stay away from. Old Oracle of VDB world.&lt;/li&gt;
&lt;li&gt;For anything that can hit production, FAISS is a no-go, it's just a lib. For simple play-around hobby projects? Why not.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Yes, this was written by a human in a 4h long flow state powered act of uninterrupted creativity. It was fun, I've actually learned a lot. &lt;br&gt;
I've written this in one go almost, except minor stuff or including the feedback from Jeff. I'll leave it mostly unedited, with original wording and typos.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>vectordatabase</category>
    </item>
    <item>
      <title>Level up your Python tooling - black, isort and other tools</title>
      <dc:creator>Olaf Górski</dc:creator>
      <pubDate>Thu, 08 Dec 2022 17:17:51 +0000</pubDate>
      <link>https://dev.to/grski/level-up-your-python-tooling-black-isort-and-other-tools-55kp</link>
      <guid>https://dev.to/grski/level-up-your-python-tooling-black-isort-and-other-tools-55kp</guid>
      <description>&lt;p&gt;Formatting and static analysis of python code and it's tooling. The lazy man’s approach to assuring Python code quality.&lt;/p&gt;

&lt;p&gt;Based on this article I also did a presentation - &lt;a href="https://github.com/grski/knowledge/blob/master/presentations/Black%2C%20isort%2C%20bandit%20and%20other%20tools.pdf"&gt;Python (anti) Patterns 002 - Black, isort, bandit&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pipelines
&lt;/h2&gt;

&lt;p&gt;What are they and why do we need them&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automating stuff? Pipelines to the rescue&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When we want to care about our Python code quality, we usually want to care about things like formatting, consistent import patterns, security and keeping our standars up to date. Ifwe want to do that in our repo/in the cloud automatically, we can use pipelines.&lt;/p&gt;

&lt;p&gt;Pipelines are simply a set of steps that constitute our &lt;strong&gt;CI/CD process.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s more or less just a piece of code that does some steps for us. Usually pipelines are defined as a yaml file that definies what steps/actions we want to take as a part of our CI/CD process, meaning analysing, checking for quality, formatting of our code and building/deploying it.&lt;/p&gt;

&lt;p&gt;In this presentation I’dlike to Focus on the steps related to automation of the quality assurance process of developing Python apps.&lt;/p&gt;

&lt;p&gt;Most commonly known tools for this in the cloud include: &lt;strong&gt;GitHub Actions, GitLab CI/CD, Bitbucket Pipelines, CircleCI, Azure DevOps.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Usually these are the things that fire up when we for example create a merge/pull request, push some code to the repo, merge one branch into the other. They trigger various checks, builds, tests and what not.&lt;/p&gt;

&lt;p&gt;The flow is like so:&lt;/p&gt;

&lt;p&gt;Trigger is received (eg. Branch is pushed to the repo) -&amp;gt; pipeline is fired -&amp;gt; various checks are made -&amp;gt; based on that pipeline can fail or succeed&lt;/p&gt;

&lt;p&gt;Other than pipelines being there up in the cloud, I consider some parts of them an integral part of local development too. Mostly the parts related to the stuff about quality control.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What makes a good code?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Nowadays the trend in Python is to take care about certain things that while not crucial, over time contribute to the project’s quality, readability and maintanability.&lt;/p&gt;

&lt;p&gt;On a high level, in my book, any piece of Python code can use some of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Consistent formatting&lt;/li&gt;
&lt;li&gt;Ordered imports that are split by sections&lt;/li&gt;
&lt;li&gt;Absolute imports&lt;/li&gt;
&lt;li&gt;Usage of modern standards that are compliant to latest standards&lt;/li&gt;
&lt;li&gt;Lack of unused imports and variables&lt;/li&gt;
&lt;li&gt;Security/vulnerability scans&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Further on we will talk how to handle this in Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  Black
&lt;/h2&gt;

&lt;h3&gt;
  
  
  *&lt;em&gt;Few words on formatting and black *&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;More often than not in projects that are not so automated and could use some of dem good tooling, you can find people in the pull requests arguing which formatting is better. How to change the formatting? Which one is better? Which one is more pep8 compliant?&lt;/p&gt;

&lt;p&gt;It can be a nightmare that is as counter productive as it gets.&lt;/p&gt;

&lt;p&gt;To get us rid of such problems and have it handled for us we use black in Python. Black is a code formatter that, well, just formats the code for you. You can make black automatically format your code before you commit. This way you can prevent any kind of arguments about pep8 and code formatting preferences of reviewers/authors, making the whole project have consistent formatting pattern, making it easier to read and so on. The easier code is to read, the better. It’s the lazy man approach. If you know what to expect, you won’t be surprised. The less you have to take care of, the better.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
               &lt;span class="n"&gt;s&lt;/span&gt;
               &lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="n"&gt;is_unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
         &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gets turned into this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example from geeksforgeeks.org.&lt;/p&gt;

&lt;h3&gt;
  
  
  ' vs "
&lt;/h3&gt;

&lt;p&gt;One thing worth noting is the fact that Python as a Language allows for the usage of both ' and " to mark strings. Black by default prefers double quotes over single. Why? Readability, usage of single quote in English language and the need to escape it everytime we use it inside our strings, it’s harder to mistake with   sign.&lt;/p&gt;

&lt;p&gt;So on so forth. One may argue here, I stand united with the double quote crowd as IMO it’s the better approach. Readability is king.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isort
&lt;/h2&gt;

&lt;p&gt;Have ya heard about imports sorting? It makes sense&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Why you should sort your imports properly&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The bigger the project we work on, usually the more stuff we import from other pieces of the code.&lt;/p&gt;

&lt;p&gt;As time goes by, these imports can become messy. It’s often the case. Isort is something that helps us with that by optimising our imports, sorting them properly, alphabetically, grouping them in sections and so on. I know this can look like a minor thing, but it’s these minor things that overall add to general code quality. Now look at the images below, the left one is before isort, right one is after it. Which one is more readable to you?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;my_lib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Object&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;my_lib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Object3&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;my_lib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Object2&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;third_party&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lib15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib14&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;absolute_import&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;third_party&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lib3&lt;/span&gt;

&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hey"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"yo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gets turned into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;absolute_import&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;third_party&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lib1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="n"&gt;lib9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lib15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;my_lib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Object2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Object3&lt;/span&gt;

&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hey"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"yo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Absolufy-imports
&lt;/h2&gt;

&lt;p&gt;The new standard is to have absolute imports. Why that is you can read on your own. There were multiple debates regarding that, the result of which is: when you can prefer absolute imports. They make for less ambiguity and provide clearer distinction of what we are really using, from which package.&lt;/p&gt;

&lt;p&gt;We also have a tool for that which is absolufy-imports. This tool is especially usefull when dealing with older projects where you might need to fix the imports in a lot of files to fit the new convention. This tool does that for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;.notifications.some_important_file&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SomeClass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;.another_important_file&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AnotherClass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gets turned into this:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;em.jobs.notifications.some_important_file&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SomeClass&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;em&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;another&lt;/span&gt;\&lt;span class="n"&gt;_important_file&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;AnotherClass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bandit
&lt;/h2&gt;

&lt;p&gt;Static analysis of our code for potential security threads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why sometimes you need a bandit in your life
&lt;/h3&gt;

&lt;p&gt;When we write our code we should have security in mind. Unless you sometimes want to make your company vulnerable to potentially losing millions. I’m going overboard with this example, but still. Security is important.&lt;/p&gt;

&lt;p&gt;Somehow we can make mistakes simple because of forgetfulness and negligence that could have been prevented otherwise. To remind us of this there are various tool that you can use.&lt;/p&gt;

&lt;p&gt;Among them is bandit. Bandit is a static analysis tool that scans your code for potentially unsafe fragments of code and warns you about them. When you run bandit against your code you’ll get a report like this and a list of where in code the potential problems are.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;Code&lt;/span&gt; &lt;span class="n"&gt;scanned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;52868&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="n"&gt;skipped&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="c1"&gt;#nosec): 0 
&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
  &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; 
    &lt;span class="n"&gt;Undefined&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; 
    &lt;span class="n"&gt;Low&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;105&lt;/span&gt; 
    &lt;span class="n"&gt;Medium&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;38&lt;/span&gt; 
    &lt;span class="n"&gt;High&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; 

&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; 
  &lt;span class="n"&gt;Undefined&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; 
  &lt;span class="n"&gt;Low&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; 
  &lt;span class="n"&gt;Medium&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; 
  &lt;span class="n"&gt;High&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;117&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  autoflake
&lt;/h2&gt;

&lt;p&gt;The less you have…&lt;/p&gt;

&lt;h3&gt;
  
  
  Reducing waste
&lt;/h3&gt;

&lt;p&gt;Sometimes it so happens that we may have unused import statements in our code or unused variables. Happens to the best. In order to automatically take care of those we may want to include autoflake in our projects.&lt;/p&gt;

&lt;p&gt;It’s a tool that simply takes care of that – removing unused imports and variables.&lt;/p&gt;

&lt;p&gt;No magic here.&lt;/p&gt;

&lt;h2&gt;
  
  
  pyupgrade
&lt;/h2&gt;

&lt;p&gt;This piece of software automatically upgrades some old syntax patterns to newer ones. That’s it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;([(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s"&gt;'{0} {1}'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;'{} {}'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s"&gt;'{0}'&lt;/span&gt; &lt;span class="s"&gt;'{1}'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;'{}'&lt;/span&gt; &lt;span class="s"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples can be found above.&lt;/p&gt;

&lt;h2&gt;
  
  
  bumpversion
&lt;/h2&gt;

&lt;p&gt;There’s this thing we call semantic versioning or semver. It’s a convention that tells us to version our code according to the following pattern: MAJOR.MINOR.PATCH&lt;/p&gt;

&lt;p&gt;For example: v0.2.12&lt;/p&gt;

&lt;p&gt;Major piece is incremented when we go for major rollouts that change A LOT.&lt;/p&gt;

&lt;p&gt;Minor piece is incremented when we do normal releases eg with bigger features.&lt;/p&gt;

&lt;p&gt;Patch is something we use for smaller features, patches, fixes etc. This one grows the fastest.&lt;/p&gt;

&lt;p&gt;In order for us to not have to manage it manually, we have a tool called bumpversion. It updates the version, creates a commit with the changes, creates a git tag and so on, all automatically. It’s a neat little piece of tooling to have in you CI/CD.&lt;/p&gt;

&lt;p&gt;This makes it easier to manage versions, create changelogs, filter commits and spot changes, bugs, versioning of your packages/api etc.&lt;/p&gt;

&lt;p&gt;You can see example commit message history and bumpversion usage here, in my project's commit history - &lt;a href="https://github.com/grski/braindead/commits/develop"&gt;braindead&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do we run all of these by hand?&lt;/p&gt;

&lt;p&gt;No. We want to be lazy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git hooks &amp;amp; pre-commit
&lt;/h2&gt;

&lt;p&gt;Automate boring tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Git hooks and pre-commit
&lt;/h3&gt;

&lt;p&gt;If you want to make all of this happen automatically, you can create git hooks that are fired eg. When you commit or before the commit. One way is to just &lt;strong&gt;create .pre-commit file and put it in your .git&lt;/strong&gt; folder and leverage eg. &lt;strong&gt;Makefile&lt;/strong&gt; or use something like &lt;strong&gt;pre-commit&lt;/strong&gt; tool.&lt;/p&gt;

&lt;p&gt;It’s a nice handy tool that handles this for you. You need to install it and create config for it to tell it which things to do before the commit. No magic here.&lt;/p&gt;

&lt;p&gt;I’ll let you google the details yourself☺&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Makefile
&lt;/h2&gt;

&lt;p&gt;Below you can find an example of a bit outdated Makefile that I use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;  &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nv"&gt;$(PATH)&lt;/span&gt;
&lt;span class="nv"&gt;SHELL&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; /bin/bash

&lt;span class="nl"&gt;flake&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    flake8 &lt;span class="nt"&gt;-v&lt;/span&gt; ./

&lt;span class="nl"&gt;isort&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    isort &lt;span class="nt"&gt;--check-only&lt;/span&gt; &lt;span class="nt"&gt;--diff&lt;/span&gt; ./

&lt;span class="nl"&gt;isort-inplace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    isort ./

&lt;span class="nl"&gt;bandit&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    bandit &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="s1"&gt;'./styles/*'&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; ./

&lt;span class="nl"&gt;black&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    black &lt;span class="nt"&gt;--check&lt;/span&gt; &lt;span class="nt"&gt;--line-length&lt;/span&gt; 120 &lt;span class="nt"&gt;--exclude&lt;/span&gt; &lt;span class="s2"&gt;"/(&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;eggs|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;git|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;hg|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;mypy _cache|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;nox|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;tox|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;venv|_build|buck- out|build|dist|migrations|node_modules)/"&lt;/span&gt; ./

&lt;span class="nl"&gt;linters&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    make flake
    make isort
    make bandit
    make black

&lt;span class="nl"&gt;bumpversion&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    bumpversion &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s1"&gt;'[skip ci] Bump version: {current_version} → {new_version}'&lt;/span&gt; &lt;span class="nt"&gt;--list&lt;/span&gt; &lt;span class="nt"&gt;--verbose&lt;/span&gt; &lt;span class="nv"&gt;$(part)&lt;/span&gt;

&lt;span class="nl"&gt;black-inplace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    black &lt;span class="nt"&gt;--line-length&lt;/span&gt; 120 &lt;span class="nt"&gt;--exclude&lt;/span&gt; &lt;span class="s2"&gt;"/(&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;eggs|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;git|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;hg|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;mypy _cache|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;nox|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;tox|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;venv|_build|buck- out|build|dist|migrations|node_modules)/"&lt;/span&gt; ./

&lt;span class="nl"&gt;autoflake-inplace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    autoflake &lt;span class="nt"&gt;--remove-all-unused-imports&lt;/span&gt; &lt;span class="nt"&gt;--in-place&lt;/span&gt; &lt;span class="nt"&gt;--remove-unused-variables&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;--exclude&lt;/span&gt; &lt;span class="s2"&gt;"styles"&lt;/span&gt; ./

&lt;span class="nl"&gt;format-inplace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    make black-inplace
    make autoflake-inplace
    make isort-inplace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Black, isort, absolufy-imports, pyupgrade, autoflake, bandit, bumpversion are tools that will make your life a bit easier.&lt;/p&gt;

&lt;p&gt;Maybe it's a good idea to include them in your local development flow and pipelines?&lt;/p&gt;

</description>
      <category>python</category>
      <category>beginners</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Pyenv, poetry and other rascals - modern Python dependency and version management</title>
      <dc:creator>Olaf Górski</dc:creator>
      <pubDate>Thu, 01 Dec 2022 07:52:00 +0000</pubDate>
      <link>https://dev.to/grski/pyenv-poetry-and-other-rascals-4g5l</link>
      <guid>https://dev.to/grski/pyenv-poetry-and-other-rascals-4g5l</guid>
      <description>&lt;h1&gt;
  
  
  Poetry, pyenv and other rascals
&lt;/h1&gt;

&lt;p&gt;On modern Python versions, environments and dependencies management. &lt;/p&gt;

&lt;h2&gt;
  
  
  PIP
&lt;/h2&gt;

&lt;p&gt;Pip is a tool most of you should already know. It's used to install packages used in python development and since a couple of version already, is shipped with Python by default. But what does it exactly mean to install packages?&lt;/p&gt;

&lt;p&gt;In short pip just provides tooling around downloading packages from Python Package Index – PYPI. It's a default index of Python packages where almost anyone can add packages. Default is a good word, because pip allows us to use different indexes. So for example your company could have it's self-hosted version of packages and then use it as a private version of pypi. This allows for example better packages verification, only private network calls during CI/CD/development processes. It's quite interesting option especially given latest malicious attacks on popular Python open source packages. &lt;/p&gt;

&lt;h3&gt;
  
  
  Package index
&lt;/h3&gt;

&lt;p&gt;What exactly is a &lt;strong&gt;Package Index?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Actually nothing complex. &lt;strong&gt;It's just a http server,&lt;/strong&gt; let's say, that provides &lt;strong&gt;a list of bundles of Python code – packages&lt;/strong&gt; and &lt;strong&gt;some metadata&lt;/strong&gt; about them. Nothing more. &lt;/p&gt;

&lt;p&gt;Fun take-home assignment to experiment with something new: &lt;strong&gt;try to implement your own version of pypi and add there certain features like token-&lt;/strong&gt;&lt;strong&gt;protected package access or even more tokens with granular permission feature.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Command overview
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt;
&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="s"&gt;"package&amp;gt;=1.0"&lt;/span&gt;
&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;
&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="err"&gt;–&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="n"&gt;requirements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;

&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;uninstall&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt;

&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="c1"&gt;#  lists all packages globally
&lt;/span&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;outdated&lt;/span&gt;

&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;freeze&lt;/span&gt;  &lt;span class="c1"&gt;#  lists packages globally but without dependencies of pip and build stuff
&lt;/span&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;freeze&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;requirements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;  &lt;span class="c1"&gt;# most naive way of dependency management =
&lt;/span&gt;
&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt;
&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;search&lt;/span&gt; &lt;span class="s"&gt;"query"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Default package installation
&lt;/h3&gt;

&lt;p&gt;Usually the case is that we have one, maximum two backwards incompatible versions of Python installed on our machine. In the past it used to be Python2 &amp;amp; Python3, Nowadays most of the time just Python3 is installed as Python2 reached EoL.&lt;/p&gt;

&lt;p&gt;Anyhow. This means that in the Dark Ages or by default one would install the packages globally, for the whole system. That’s is bad for multitude of reasons. As for what installing package means, in a very big summary, it’s nothing more than downloading a bundle of python code structured in a certain way, that gets downloaded and put in a given directory of python installation, with additional steps possible inbetween.&lt;/p&gt;

&lt;p&gt;What if project A requires package Z in version 1.0.0, but project B requires package Z in version 2.0.0? Would you reinstall this package every time you switch to different projects?&lt;/p&gt;

&lt;h2&gt;
  
  
  virtualenv
&lt;/h2&gt;

&lt;p&gt;To combat the problem described in the previous paragraph -&amp;gt; packages getting installed globally, &lt;strong&gt;virtualenv&lt;/strong&gt; came around. In short, it’s something that allows us to "create" another, "instance" of &lt;strong&gt;installation&lt;/strong&gt; &lt;strong&gt;of&lt;/strong&gt; &lt;strong&gt;Python&lt;/strong&gt;. Eg. For a given project only instead of system-wide.&lt;/p&gt;

&lt;p&gt;This way we can have various python package versions for various projects.&lt;/p&gt;

&lt;p&gt;Subset of virtualenv functionalities comes integrated with &lt;strong&gt;default&lt;/strong&gt; &lt;strong&gt;CPython&lt;/strong&gt; &lt;strong&gt;installation&lt;/strong&gt; &lt;strong&gt;from version 3.3&lt;/strong&gt; &lt;strong&gt;onwards&lt;/strong&gt; as &lt;code&gt;venv&lt;/code&gt; module.&lt;/p&gt;

&lt;h2&gt;
  
  
  Poetry
&lt;/h2&gt;

&lt;p&gt;What if pip and virtualenv had a love child that was also doing steroids? Well, we'd obtain Poetry.&lt;/p&gt;

&lt;p&gt;The problem with pip is usually dependency version management.&lt;/p&gt;

&lt;p&gt;Even if we know, our project A, requires package Z in version 1.0.0, usually at the first glance, pip doesn’t tell us about the dependencies of this Z package.&lt;/p&gt;

&lt;p&gt;It introduces the possibility of problems when your project reaches a point where it has a little bit more packages installed. Because these packages also have dependencies and their dependencies also have them. &lt;/p&gt;

&lt;p&gt;Usually it’s not a dependency hell like in the JS worlds, but at some point it can also get a bit tricky if you only lock the dependencies at the top level.&lt;/p&gt;

&lt;p&gt;And at some point, when you reach corporate-level size of a project, it’s almost guaranteed to have problems with this. Also if the versions of these dependencies aren’t guaranteed by default what about debugging? &lt;/p&gt;

&lt;p&gt;I mean one build could have versions 1.2.3 of some dependency of a dependency, but another build, done 10 minutes earlier could have 1.2.2 if the versions aren’t resolved in a deterministic, guaranteed way. It enables nasty bugs to appear.&lt;/p&gt;

&lt;p&gt;This also is a security risk, because if you do not know what version of the dependency exactly you have, &lt;strong&gt;a&lt;/strong&gt; &lt;strong&gt;malicious&lt;/strong&gt; &lt;strong&gt;version&lt;/strong&gt; &lt;strong&gt;might&lt;/strong&gt; find their way in without our explicit knowledge, which is a &lt;strong&gt;vulerability&lt;/strong&gt; &lt;strong&gt;introduction&lt;/strong&gt; &lt;strong&gt;opportunity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We have something called &lt;strong&gt;dependency&lt;/strong&gt; &lt;strong&gt;resolving&lt;/strong&gt; and &lt;strong&gt;dependency&lt;/strong&gt; &lt;strong&gt;locking&lt;/strong&gt;*.&lt;/p&gt;

&lt;p&gt;Basically it’s just a proces of making sure that &lt;strong&gt;we&lt;/strong&gt; &lt;strong&gt;know&lt;/strong&gt; &lt;strong&gt;the&lt;/strong&gt; &lt;strong&gt;dependencies&lt;/strong&gt; &lt;strong&gt;of&lt;/strong&gt; &lt;strong&gt;our&lt;/strong&gt; &lt;strong&gt;dependencies&lt;/strong&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;strong&gt;their&lt;/strong&gt; &lt;strong&gt;dependencies&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And also we have a clear account of their &lt;strong&gt;versions&lt;/strong&gt;,&lt;strong&gt;usually&lt;/strong&gt; &lt;strong&gt;signed&lt;/strong&gt; &lt;strong&gt;with a&lt;/strong&gt; &lt;strong&gt;hash&lt;/strong&gt;*.&lt;/p&gt;

&lt;p&gt;This allows something called deterministic builds which is one of the keys of &lt;strong&gt;modern CI/&lt;/strong&gt;&lt;strong&gt;CDs&lt;/strong&gt; and apps that adhere to &lt;strong&gt;the&lt;/strong&gt; &lt;strong&gt;Twelve-factor&lt;/strong&gt; &lt;strong&gt;app&lt;/strong&gt; &lt;strong&gt;pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is exactly what Poetry does and it does this well. &lt;/p&gt;

&lt;p&gt;Other than that, while we are at it, poetry &lt;strong&gt;also&lt;/strong&gt; &lt;strong&gt;makes&lt;/strong&gt; &lt;strong&gt;projecet&lt;/strong&gt; &lt;strong&gt;management&lt;/strong&gt; &lt;strong&gt;easier&lt;/strong&gt;, takes care of &lt;strong&gt;creating&lt;/strong&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;strong&gt;managing&lt;/strong&gt; &lt;strong&gt;virtualenvs&lt;/strong&gt; &lt;strong&gt;for&lt;/strong&gt; &lt;strong&gt;you&lt;/strong&gt; and enables easier, more &lt;strong&gt;centralised&lt;/strong&gt; &lt;strong&gt;project&lt;/strong&gt; &lt;strong&gt;configuration&lt;/strong&gt; &lt;strong&gt;by&lt;/strong&gt; &lt;strong&gt;introducing&lt;/strong&gt; &lt;strong&gt;pyproject.toml&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pyproject.toml&lt;/strong&gt; is usually the new standard python package config file.&lt;/p&gt;

&lt;p&gt;Oh also, it makes building the packages easier as &lt;strong&gt;it&lt;/strong&gt; &lt;strong&gt;can&lt;/strong&gt; &lt;strong&gt;bundle&lt;/strong&gt; &lt;strong&gt;your&lt;/strong&gt; &lt;strong&gt;python&lt;/strong&gt; &lt;strong&gt;code&lt;/strong&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;strong&gt;publish&lt;/strong&gt; &lt;strong&gt;it&lt;/strong&gt; &lt;strong&gt;to the&lt;/strong&gt; &lt;strong&gt;package&lt;/strong&gt; &lt;strong&gt;index of&lt;/strong&gt; &lt;strong&gt;your&lt;/strong&gt; &lt;strong&gt;choice.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Overall poetry is neat. Very neat.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example pyproject.toml
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.poetry]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"django-boilerplate"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
&lt;span class="py"&gt;authors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Your Name &amp;lt;you@example.com&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[tool.poetry.dependencies]&lt;/span&gt;
&lt;span class="py"&gt;python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"^3.10"&lt;/span&gt;
&lt;span class="py"&gt;django&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"^4.0.6"&lt;/span&gt;
&lt;span class="py"&gt;psycopg2-binary&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"^2.9.3"&lt;/span&gt;
&lt;span class="nn"&gt;celery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="py"&gt;extras&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;["redis"]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"^5.2.7"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="nn"&gt;[tool.poetry.dev-dependencies]&lt;/span&gt;
&lt;span class="py"&gt;coverage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"^6.4.2"&lt;/span&gt;

&lt;span class="py"&gt;isort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"^5.10.1"&lt;/span&gt;
&lt;span class="py"&gt;flake8&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"^4.0.1"&lt;/span&gt;
&lt;span class="py"&gt;black&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"^22.6.0"&lt;/span&gt;

&lt;span class="nn"&gt;[build-system]&lt;/span&gt;
&lt;span class="py"&gt;requires&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;["poetry-core&amp;gt;=1.0.0"]&lt;/span&gt;
&lt;span class="py"&gt;build-backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"poetry.core.masonry.api"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pyenv
&lt;/h2&gt;

&lt;p&gt;Python is a peculiar little animal that sheds its skin from time to time. Meaning Python itself, other than our dependencies, also has it’s own versions. Each version contains new features, various improvements. Some of them are sometimes even backwards incompatibile.&lt;/p&gt;

&lt;p&gt;By default it’s not trivial to install various python versions and have them working properly on the same machine. &lt;/p&gt;

&lt;p&gt;Why would you need that? Well, same as with dependencies. One project could depend on Python 3.10, the other on 2.7 and some other on 3.12. &lt;/p&gt;

&lt;p&gt;We need something like virtualenv, that would provide isolation, but instead of a project-level for python dependnecies, instead for the system on the python version level.&lt;/p&gt;

&lt;p&gt;How do we do that?&lt;/p&gt;

&lt;p&gt;With pyenv. Juiced up pyenv with a neat plugin that let’s us create virtualenvs from different python versions/interpreter implementations.&lt;/p&gt;

&lt;p&gt;Pyenv + pyenv-virtualenv is also nice in regard of integration with poetry.&lt;/p&gt;

&lt;p&gt;Anyhow. So we have pyenv-virtualenv which is a virtualenv-like wrapper for pyenv, which in turn is a a wrapper around python versions management, working on poetry which is a wrapper for pip and pip-tools, integrated with virtualenv which is also kind of a wrapper.&lt;/p&gt;

&lt;p&gt;So we have wrapper of a wrapper working on a wrapper of a wrapper. Wrapper-ception.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://asciinema.org/a/pMssjDmLN79NuJT8rr2VQg6GW"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3tiSwG3z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://asciinema.org/a/pMssjDmLN79NuJT8rr2VQg6GW.svg" alt="asciicast" width="880" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Short how-to
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install pyenv from official repository &lt;code&gt;curl https://pyenv.run | bash&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;After installation run &lt;code&gt;exec $SHELL&lt;/code&gt; to restart shell and apply the changes&lt;/li&gt;
&lt;li&gt;Install desired version of python runtime (eg. 3.6.15) &lt;code&gt;pyenv install 3.6.15&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Go to the project's root directory&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;pyenv-virtualenv&lt;/code&gt; plugin from &lt;a href="https://github.com/pyenv/pyenv-virtualenv"&gt;repository&lt;/a&gt; (in never versions it's included in pyenv.run script I think)&lt;/li&gt;
&lt;li&gt;Create virtualenv &lt;code&gt;pyenv virtualenv 3.6.15 your-cool-virtualenv-name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set virtual env as local (make sure you are in project root dir) &lt;code&gt;pyenv local your-cool-virtualenv-name&lt;/code&gt; for automatic venv activation&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Piptools
&lt;/h2&gt;

&lt;p&gt;If your project is simple enough or you do not want to be bother will all of the previous things you can use pip-tools to pin your dependencies and all that. &lt;/p&gt;

&lt;p&gt;Check out my other articles:&lt;br&gt;
&lt;a href="https://dev.to/grski/tenancy-pattern-in-a-saas-product-2ea0"&gt;Tenancy pattern in a SaaS product&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/grski/how-i-created-my-own-blogging-system-in-less-than-100-lines-of-code-305b"&gt;How I created my own blogging system in less than 100 lines of code&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/grski/three-types-of-managers-which-one-are-you-5197"&gt;Three types of managers - which one are you?&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/grski/readme-file-structure-and-sections-pm8"&gt;How to write README files like a pro&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>tutorial</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
    <item>
      <title>Tenancy pattern in a SaaS product</title>
      <dc:creator>Olaf Górski</dc:creator>
      <pubDate>Wed, 30 Nov 2022 01:15:27 +0000</pubDate>
      <link>https://dev.to/grski/tenancy-pattern-in-a-saas-product-2ea0</link>
      <guid>https://dev.to/grski/tenancy-pattern-in-a-saas-product-2ea0</guid>
      <description>&lt;p&gt;Understanding/using multi-tenant architecture in Django and it's importance in SaaS product from the perspective of my protegee, a junior engineer at thirty3 - Dominik. Without further ado, I'll let him do the writing. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4mv7kqkg433zyhagxdlp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4mv7kqkg433zyhagxdlp.png" alt="Thank you, Marta Buriak, for the illustration." width="663" height="748"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Disclaimer: this post was written couple of years ago by me and Dominik Szady, who was my protegee at that time. I'm just publishing it now on my blog.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Junior Introduction
&lt;/h2&gt;

&lt;p&gt;Hello there! My name is Dominik, I am a junior developer at thirty3, recruit among a bunch of professionals.&lt;/p&gt;

&lt;p&gt;An environment that'll be challenging, but is probably the best one for a beginner like me to be in. Place where there's a mentor which can help me in tough times, answer all my questions and point the way, tell me about mistakes I make.&lt;/p&gt;

&lt;p&gt;Does it make learning programming easier than it was before? Hell yes! Does it make it easy? Hell no!&lt;br&gt;
Today I'd like to write a couple of words about my experience so far, new tasks, mentoring and so on, in a collaborative article together with my mentor - Olaf, and most important - about tenancy in software architecture.&lt;/p&gt;

&lt;p&gt;I would say the process of learning could be divided into two parts:&lt;/p&gt;

&lt;p&gt;understanding a new issue (technology, tool, etc.) which ends up in one having a general grasp of how things are done, that allows you to build things based on an example, do slight modifications to existing stuff and so on;&lt;/p&gt;

&lt;p&gt;the never-ending process of mastery which leads to one being able to create complex stuff from the ground up;&lt;/p&gt;

&lt;p&gt;For me, being a junior programmer means that I'll often face problems that will require me to do the first part - learn something new to solve them. This is exactly how I could describe my first month at thirty3 - being out of my programming comfort zone and doing things I have never done before. Which is GREAT.&lt;/p&gt;
&lt;h2&gt;
  
  
  First days
&lt;/h2&gt;

&lt;p&gt;First days at a new work are always tough and I swear when I set up my working environment at previous companies, something always went wrong - I was lacking some tools, packages, getting strange errors. Fortunately, running the project at thirty3 for the first time was quite the opposite.&lt;/p&gt;

&lt;p&gt;The difference-maker, in this case, was the combination of Docker and Makefile. All I had to to was basically download docker (and docker-compose) for my PC and follow the damn README.md to have everything up and running. App is running. Documentation is there, practices are defined, code is clear and easy to understand, tests are there. It was a breeze.&lt;/p&gt;

&lt;p&gt;At least until some point. It did not take a long time for me to get hit by multi-tenant architecture. What is that?&lt;br&gt;
Imagine you have an application used by multiple companies (tenants).&lt;br&gt;
You would like to make sure that you don't accidentally leak data between companies while making the architecture scalable and efficient.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tenants to the rescue
&lt;/h2&gt;

&lt;p&gt;At thirty3 we use Django-tenants to solve this problem, at least in a lot of cases. There is one database instance to store data for our clients, but multiple schemas - one for every tenant (company). It creates logical separation between the data.&lt;br&gt;
Before jumping to examples of how I tried understanding this concept, I will break it down for you so, hopefully, you will immediately notice my mistakes and get a grasp of how things work.&lt;br&gt;
Let's assume we have a very simple Django project that allows companies to create projects on which they'll work.&lt;br&gt;
We need two applications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;companies
projects
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And models declared in respective models.py files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# `companies/models.py`
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TenantMixin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# `projects/models.py`
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;is_paid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BooleanField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The behavior we would like to have is that each Company has its Projects that are not accessible to other companies.&lt;/p&gt;

&lt;p&gt;As I mentioned earlier every tenant has its schema in the database. It is created with the creation of a model that inherits from TenantMixin (Company).&lt;br&gt;
The thing that allows us to distinguish tenants is a unique schema name (TenantMixin attribute) which we need to provide with the creation of every Company object. (Olaf's remark: it doesn't have to be schema name - it can be ID or almost anything really, as long as it's unique. Overall schema_name is just a metadata that lets us know where to search in the db)&lt;/p&gt;

&lt;p&gt;Besides that, we need to create a specific schema called "public" whose purpose (O: Actually it's there in postgres by default, we just create the model in our table with tenants) is to store the global data not specific for a given tenant/Company and all the tenant schema names.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tenants in Django
&lt;/h2&gt;

&lt;p&gt;The question we should ask now is: how Django knows which data should be stored in "public" schema and which in schemas for specific tenants?&lt;br&gt;
It is done in settings file be setting up SHARED_APPS and TENANT_APPS variables. These are lists of Django applications (just like INSTALLED_APPS). Putting application in eg. TENANT_APPS will mean that tables for Models from this application will be created in each of tenant schemas. On the other side if we add our application in SHARED_APPS list, as we except tables will be created in "public" schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SHARED_APPS = ["companies", …]
TENANT_APPS = ["projects", …]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another question is how does Django know on which tenant schema actions should be performed?&lt;br&gt;
Tenants are identified by URL, eg. requesting an URL "tenant.something.com" would result in hostname being searched in an appropriate table in "public" schema. If the match is found, the schema context is updated which means that queries will be performed at matched tenant schema.&lt;br&gt;
Django-tenants provides some utils to set the schemas from the code perspective.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;schema_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# queries will be performed against the schema "schema_name"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;tenant_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;#queries will pe performed against the schema of tenant_object.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Knowing all of this let's take a look at the code snippets below to identify some mistakes I made during the thought process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantsTestCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseTenantTestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_tenants_example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;companies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;​        ...&lt;br&gt;
The expected behavior for me would be get all the companies, the result was empty QuerySet. Ok, maybe I need to create one, lets try.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantsTestCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseTenantTestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt; &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="n"&gt;test_schema&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_tenants_example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;companies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time i received an error&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Can't create tenant outside the public schema. Current schema is test&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I asked myself "What is going on?", but managed to find somewhere the usage of &lt;code&gt;schema_context&lt;/code&gt;. So i gave it a try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantsTestCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseTenantTestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt; &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="n"&gt;test_schema&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_tenants_example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;companies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great, no error this time. Anyway, the companies variable is still empty QuerySet. One last try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantsTestCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseTenantTestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;schema_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt; &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="n"&gt;test_schema&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt; &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="n"&gt;test_schema&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_tenants_example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;schema_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;companies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, this time I got a QuerySet of two Company objects. But wait, I created one. Time to bring it all together. &lt;/p&gt;

&lt;p&gt;It turns out that when you run tests with Django-tenants, new tenant, with schema_name "test" is created and all the queries are performed against this schema unless we switch it. (O: At least in our case because we use FastTenant case -&amp;gt; otherwise you'll be creating new tenants too often and they take too long to run)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantsTestCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseTenantTestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# schema_context = "test"
&lt;/span&gt;        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;schema_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="c1"&gt;# schema_context = "public"
&lt;/span&gt;            &lt;span class="n"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt; &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="n"&gt;test_schema&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_tenants_example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# schema_context = "test"
&lt;/span&gt;        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;schema_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="c1"&gt;# schema_context = "public"
&lt;/span&gt;            &lt;span class="n"&gt;companies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# schema_context = "test"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's remember that Company which is our tenant object is stored in "public" schema so the empty QuerySets we received earlier were correct because we tried searching for Company object in schemas that do not contain them.&lt;br&gt;
Going further, the creation of Project object needs to be done in the context of the specific tenant schema as this is the place where its tables are stored.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantsTestCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseTenantTestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_tenants_example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="c1"&gt;# Here we can create project, as we are in context of “test” 
&lt;/span&gt;       &lt;span class="c1"&gt;# tenant
&lt;/span&gt;       &lt;span class="n"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_paid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;schema_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="c1"&gt;# Here we can not create Project, we are in “public” 
&lt;/span&gt;            &lt;span class="c1"&gt;#context,no tables for Project here
&lt;/span&gt;            &lt;span class="n"&gt;companies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For me working with tenants resolves around tracking how the context changes to always know what queries can I perform and what effects to expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mentor's take
&lt;/h2&gt;

&lt;p&gt;Okay. Enough from Dominik's perspective. Now it's my time to blabber.&lt;br&gt;
Let's give you some context on a broader level.&lt;br&gt;
What you've read so far is Dominik's understanding of the Tenancy concept and how we use it at thirty3. He's more or less correct, some things are oversimplified but the general idea is there. A little proud of him, took me way longer to grasp certain things.&lt;br&gt;
I'll try to give you more information regarding the decision process we had when we started using tenants, why we use them and why you might want to do that too.&lt;/p&gt;

&lt;p&gt;Let's start.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are tenants?
&lt;/h2&gt;

&lt;p&gt;First thing - tenants. What are they? It's this concept, mostly used in eg. SaaS products, that, if you simplify things, they're your clients. I at least like to think in that way. When you create a bespoke solution for a given Company, only that given Company is the user there, most of the time that is.&lt;/p&gt;

&lt;p&gt;Some things are defined globally in that DB, other things are defined and should be accessible only for that Company and so on. In a normal case where only that company will be using the app, you don't have to think much about that. A problem arises when you want to globalize that app and have multiple companies. All of them have some private data, some public data. This data should be separated and not accessible by other Companies using that given SaaS. You need another layer of abstraction that logically binds or encapsulates this Company data. Aw shiet, here go tenants. What are other benefits?&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling a SaaS
&lt;/h2&gt;

&lt;p&gt;As times go and our apps grow, when you start receiving clients that are not your immediate family nor your investors, things start to get more complicated. Privacy is suddenly important. Data breaches/leaks are costly. Then the product starts getting traction, your user base grows, optimization becomes a problem. It happens in almost every successful product.&lt;/p&gt;

&lt;p&gt;Good thing is to think about these problems and prepare for them but only as much as you need to, so you don't over engineer. In our case, most of the time, we decided to use Tenant pattern for that, using DB schemas to realize it. It makes it harder for us to leak our client's data and easier to scale our app up while not putting much overhead over our development time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Methods of scaling the database
&lt;/h2&gt;

&lt;p&gt;Because what's the limiting factor most of the time, in a lot of apps? DBs. What are the ways to scale DBs? Horizontal and Vertical.&lt;br&gt;
Vertical means that you have one DB on which you just throw more resources - better hardware. This scaling has its limits. Once you hit them, no matter how much money you have - that's it. Can you do something about it?&lt;/p&gt;

&lt;p&gt;Here comes Horizontal scaling, which means using more machines/DBs instead of just one. It's quite tricky too - it's not just about creating more instances of the db. Things like master-slave patterns, data consistency, network of nodes and so on come into play here. Quite a complex topic if you ask me.&lt;br&gt;
Anyways.&lt;/p&gt;

&lt;p&gt;Of course, this way has limits too, but they're often way bigger than the limits of the vertically scaled system&lt;/p&gt;

&lt;p&gt;.&lt;br&gt;
Now - managing tenants in a SaaS-like product can be done in many different ways. First is DB per client. Here we'd probably have one bigger DB with things shared globally in the app and then smaller (or bigger) DBs with data unique to the client.&lt;/p&gt;

&lt;p&gt;Second is Schema per client, which means one DB, that's almost splitted into subdbs - I'm oversimplifying but bear with me.&lt;br&gt;
The third is custom permissions and relations in tables for example with the data of all clients being put together in one schema, one db.&lt;/p&gt;

&lt;p&gt;1st one is costly and troublesome to manage at a smaller scale.&lt;/p&gt;

&lt;p&gt;3rd one often ends up in messy DB tables, privacy concerns.&lt;/p&gt;

&lt;p&gt;2nd one, however… Well, it almost puts no overhead to a situation in which you'd have plain single schema/DB architecture - not a SaaS one, but it makes it easy to scale and separate client's data by abstracting DBs - instead of having multiple DBs that are a hassle to manage, it uses one DB as if it was 'many', in a way at least.&lt;/p&gt;

&lt;p&gt;Hence, we went for it. And we are quite satisfied with the results, honestly.&lt;/p&gt;

&lt;p&gt;Also a big selling point for tenants is the fact that operating on the querysets is just way easier. Querying for Invoices only from a given company? Just set the proper context and that's all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Different methods of managing tenants
&lt;/h2&gt;

&lt;p&gt;The so-called search path that we set for postgres's queries using our db router, can be set in multiple ways. Traditional tenant pattern uses subdomains as means of identifying the tenant - eg x.myproduct.com will search for tenant x. That's one way. Catches here that we had to consider when using default identyfing model: make sure to forbid users from registering tenants with names of frequently used subdomains eg. ftp, mail, static and so on, otherwise you might be in for a nasty surprise. Also, remember to have certificates that have a wildcard for the subdomains - otherwise you'll be left without SSL for your tenants subdomains, which sucks quite a bit.&lt;/p&gt;

&lt;p&gt;Another is to for example put it as a part of the url, but not the subdomain. For example: example.com/v1/tenant/someendpoint .&lt;br&gt;
We usually use the lattter one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What tenants did for us
&lt;/h2&gt;

&lt;p&gt;We achieved:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;no additional costs of managing infrastructure&lt;/li&gt;
&lt;li&gt;in the beginning, we still can start with just one DB&lt;/li&gt;
&lt;li&gt;it's easy to scale up -&amp;gt; change the way you set the search path for the schema and you are done -&amp;gt; horizontal scaling is a breeze&lt;/li&gt;
&lt;li&gt;customer's data is separated in a better way&lt;/li&gt;
&lt;li&gt;we don't have to bother with tricky querysets&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So that'd be the small glimpse into tenants architecture we choose. Of course I just lightly touched the topic. Anyways.&lt;/p&gt;

&lt;p&gt;This article is more of an exercise for Dominik to learn to express himself and start writing, communicating, rather than something that's supposed to be extremely filled with knowledge.&lt;/p&gt;

&lt;p&gt;Not so long ago I wrote a post about my first months at thirty3, while still considering myself a Junior then. Now I'm someone who has a Junior person as their protegee. Feels nice. Growth. I like that.&lt;/p&gt;

&lt;p&gt;So, enough about this exercise. Thank you all for reading, and let's see you soon.&lt;/p&gt;

&lt;p&gt;PS: cons of having me as a mentor - you'll probably start writing.&lt;/p&gt;

&lt;p&gt;Check out my other articles:&lt;br&gt;
&lt;a href="https://dev.to/grski/three-types-of-managers-which-one-are-you-5197"&gt;Three types of managers - which one are you?&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/grski/readme-file-structure-and-sections-pm8"&gt;How to write README files like a pro&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/grski/how-i-created-my-own-blogging-system-in-less-than-100-lines-of-code-305b"&gt;How I created my own blogging system in less than 100 lines of code&lt;/a&gt;&lt;/p&gt;

</description>
      <category>welcome</category>
      <category>community</category>
      <category>learning</category>
    </item>
    <item>
      <title>How I created my own blogging system in less than 100 lines of code</title>
      <dc:creator>Olaf Górski</dc:creator>
      <pubDate>Tue, 29 Nov 2022 06:28:40 +0000</pubDate>
      <link>https://dev.to/grski/how-i-created-my-own-blogging-system-in-less-than-100-lines-of-code-305b</link>
      <guid>https://dev.to/grski/how-i-created-my-own-blogging-system-in-less-than-100-lines-of-code-305b</guid>
      <description>&lt;p&gt;Recently I started thinking about changing my blogging engine and the template that it used. It felt a bit too bloated for my tastes, outdated and messy. After a couple of months of waiting, I've finally spun into action to create &lt;a href="https://github.com/grski/braindead" rel="noopener noreferrer"&gt;braindead&lt;/a&gt; - a braindead simple static site generator with markdown and code highlighting support.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F78972hrcfmbjesc9pbr5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F78972hrcfmbjesc9pbr5.jpg" alt="Load time of grski.pl generated with braindead" width="720" height="613"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first thing that I did was to look through available solutions for blogs, blogging engines and free templates. I had experience with Pelican and Jekyll. Both were quite okay, yet felt like they're too much, some processes there were too complicated for me. Same with Hugo, GatsbyJs and the rest.&lt;/p&gt;

&lt;p&gt;I won't even mention the templates that I've seen - most of them were just too heavy. Some candidates satisfied my visual senses. They had a problem though, most of the time they used React or Vue. While both of these are marvellous pieces of Software and there are plenty of places where you should use them, this was not such a place. Why?&lt;/p&gt;

&lt;h2&gt;
  
  
  Bloatware
&lt;/h2&gt;

&lt;p&gt;I'm a minimalist that tries to live their life with as little as possible. I don't even own a bed as sleeping on the floor is comfortable enough. Minimalism allows me to focus on what truly matters. So when I'm facing current trends in Software Development, I'm lost. One, in particular, got under my skin - its name is BloatWare. I straight out hate it. For multiple reasons &lt;a href="https://tonsky.me/blog/disenchantment/" rel="noopener noreferrer"&gt;check out this post - Software Disenchantment by tonsky&lt;/a&gt;, be it the fact that most of it is slow, or the fact that through wasteful resource management, BloatWare uses way more electricity and hardware than it should. It's a small thing, but something that nonetheless adds up to environmental damage overall. Even such things do compound over time, giving a lot of impact in the end.&lt;/p&gt;

&lt;p&gt;I hate it when someone takes a simple static landing page and creates React App out of it with hundreds of js libs used and images that weight dozens of MBs. All of that while it has the same functionality as the old one. But you know, it's in react now, that's so cool, rightttt? me_crying_internally.jpg &lt;/p&gt;

&lt;p&gt;A lot of current websites could be easily made in pure HTML/CSS and maybe some JS. They'd be faster to develop and make for a better UX. Each tool has it's best uses. The problem lies in the fact that once we have a hammer, everything is a nail.&lt;/p&gt;

&lt;p&gt;Because of all that and how available solutions did not satisfy my needs completely, I've decided to create my blogging engine/static site generator. Just for the heck of it, just because I can. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpnpaisthakvqlz0yggog.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpnpaisthakvqlz0yggog.png" alt="funny meme" width="500" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's do this
&lt;/h2&gt;

&lt;p&gt;My assumptions when creating this were as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I decided to go with a stack I know - Python, Jinja, Markdown. It's crazy how much you can do just with these. &lt;/li&gt;
&lt;li&gt;Code shall be type hinted.&lt;/li&gt;
&lt;li&gt;I'll use git and GitHub to host my repo.&lt;/li&gt;
&lt;li&gt;The code will be opensource and on MIT license.&lt;/li&gt;
&lt;li&gt;I'll use black, isort, pyflakes and autopyflake for code formatting, bandit for some security scanning.&lt;/li&gt;
&lt;li&gt;poetry as dependency/packaging manager. No pipenv - it's dead.&lt;/li&gt;
&lt;li&gt;Automated builds and releases through GitHub actions.&lt;/li&gt;
&lt;li&gt;Versioning with bumpversion.&lt;/li&gt;
&lt;li&gt;Two branches: develop - bump patch, master - bump minor. Major - manual releases.&lt;/li&gt;
&lt;li&gt;Additional configuration of context/blog through TOML.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The plan was: I'll write my posts in markdown files, render them to HTML and enrich with jinja. I'll keep all my posts in a directory called &lt;code&gt;posts&lt;/code&gt; and index will be named index.md. While I'm at that I might as well add support for pages other than index.&lt;/p&gt;

&lt;p&gt;So the first piece of code I decided to create was about that - finding all the markdowns and rendering them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_all_posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;posts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt; All the md posts - both extensions .markdown and .md&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;find_all_markdown_and_md_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_all_pages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt; Similar to the one above, but searches for posts - another directory. Both .md and .markdown &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;find_all_markdown_and_md_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_all_markdown_and_md_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt; Base method that finds both .md and .markdown recursively in a given directory and it&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s children. &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;md_files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Generator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;iglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;recursive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;markdown_files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Generator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;iglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md_files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;markdown_files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To find all the &lt;code&gt;.md&lt;/code&gt; and &lt;code&gt;.markdown&lt;/code&gt; files recursively, I've used &lt;code&gt;glob&lt;/code&gt; packages and it's &lt;code&gt;iglob&lt;/code&gt; function. &lt;code&gt;iglob&lt;/code&gt; is a version of &lt;code&gt;glob&lt;/code&gt; that instead of returning an in-memory list of found filenames, returns a generator.&lt;/p&gt;

&lt;p&gt;Back in the days, I used to work with big volumes of data. Until this day I'm a little bit skewed in this regard. It's probably not possible, but someone might decide to create way too many files. Reading them all at once to memory might use too much of it. Hence the iglob instead of &lt;code&gt;glob&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We have the filenames, now time for rendering the markdown.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Markdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tables&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fenced_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;codehilite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;footnotes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_markdown_to_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt; Markdown to html. Important here is to keep the reset() method. &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To do that I've used python-markdown package with a couple of extensions for more usability. Off to Jinja rendering we go.&lt;/p&gt;

&lt;p&gt;I came up with something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_jinja_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt; Rendering jinja template with a context and global config. &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;context_with_globals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context_with_globals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now time to take care of context building for jinja templates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_meta_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    This builds context that we get from Meta items from markdown like
    post/page Title, Description and so on.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_article_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article_html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt; Contant that&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ll be used to render template with jinja. &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;article_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="nf"&gt;build_meta_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_url_to_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jinja_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt; Builds and adds url for a given page/post to jinja context. &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;jinja_context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;new_filename&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DIST_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jinja_context&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that done it was time for saving the output. I decided to preserve the structures of the posts as default behaviour. Same with the names.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original_file_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt; Saves a given output based on the original filename in the dist folder&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;new_location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DIST_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;original_file_name&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;new_directory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_directory&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makedirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_directory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TextIO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;output_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;new_location&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last thing: it'd be nice to have the statics gathered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gather_statics&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;template_statics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEMPLATE_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;static&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template_statics&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copytree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template_statics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DIST_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;static&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dirs_exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, putting it all together&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_blog&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt; Renders both pages and posts for the blog and moves them to dist folder.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;render_posts&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="nf"&gt;render_all_pages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;render_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;gather_statics&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_all_pages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt; Rendering of all the pages for the blog. markdown -&amp;gt; html with jinja -&amp;gt; html&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jinja_environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;find_all_pages&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nf"&gt;render_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;additional_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;additional_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;additional_context&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;additional_context&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;page_html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render_markdown_to_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;jinja_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page_html&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;additional_context&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render_jinja_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;jinja_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;save_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original_file_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;jinja_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_posts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jinja_environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;render_and_save_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;find_all_posts&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_and_save_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt; Renders blog posts and saves the output as html. md -&amp;gt; html with jinja -&amp;gt; html&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;article_html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render_markdown_to_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;jinja_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_article_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article_html&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;article_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render_jinja_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;jinja_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new_filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;save_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original_file_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;jinja_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slug&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;add_url_to_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jinja_context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;jinja_context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;new_filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_markdown_to_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt; Markdown to html. Important here is to keep the reset() method. &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Markdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tables&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fenced_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;codehilite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;footnotes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jinja_environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;additonal_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;articles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;render_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;additional_context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;additonal_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the core of the app, as for the small details, I won't bother you with them.&lt;/p&gt;

&lt;p&gt;The original version that I wrote had less than 100 lines of code. Since then I've added a bit of documentation, some features and so on.  Right now with all the new features and comments, I'm up to... Hm, I don't know. Let's check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;braindead-th-wknNA-py3.8&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;grski@grski braindead]&lt;span class="nv"&gt;$ &lt;/span&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; + | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
168

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;168 LoC, not bad. &lt;/p&gt;

&lt;h2&gt;
  
  
  Default template
&lt;/h2&gt;

&lt;p&gt;I decided to base on &lt;a href="https://github.com/ribice/kiss" rel="noopener noreferrer"&gt;kiss&lt;/a&gt; template for Hugo, slightly modifying and simplyfing it. I've slimmed down the css, minified it, changed the html a bit, but overall the look and feel stayed the same. I'll need to slim down the css even more in the future.&lt;/p&gt;

&lt;p&gt;I like it though - it's very minimalistic yet elegant. However if you do not like it - you can create your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;How to use braindead? Just create index.md, run &lt;code&gt;pip install braindead&lt;/code&gt; and then &lt;code&gt;braindead&lt;/code&gt;. That's it. This is the minimal requirement. Want more? Create &lt;code&gt;posts&lt;/code&gt; and &lt;code&gt;pages&lt;/code&gt; dir, put your content there.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD
&lt;/h2&gt;

&lt;p&gt;I've added github actions to the repo that lint the code with all the linters, run tests (none written so far though) and create automatic PRs if everything is okay.&lt;/p&gt;

&lt;p&gt;If a PR got merged, it results in a new release on GH and &lt;a href="https://pypi.org/project/braindead/" rel="noopener noreferrer"&gt;PyPi&lt;/a&gt; of the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Let's start with generation speed - it's quite imporant.&lt;/p&gt;

&lt;p&gt;My blog has about 50 pages now. To render it, &lt;a href="https://github.com/grski/braindead" rel="noopener noreferrer"&gt;braindead&lt;/a&gt; took:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;real    0m0,975s
user    0m0,908s
sys     0m0,060s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below 1 second. I find that acceptable.&lt;/p&gt;

&lt;p&gt;Now on to the rendering of the page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqojggb9jgvnxd9wn5uws.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqojggb9jgvnxd9wn5uws.png" alt="Load time of grski.pl generated with braindead" width="616" height="30"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Google PageSpeed Insight:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh1b6z7was0k1siscxitx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh1b6z7was0k1siscxitx.png" alt="Speed score on pagespeed insights" width="369" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is how it looks like:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxzg71sd1u21mea1j2ydx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxzg71sd1u21mea1j2ydx.png" alt="Default template of braindead" width="800" height="684"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see a live example on my blog - &lt;a href="https://grski.pl" rel="noopener noreferrer"&gt;grski.pl&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;I'm quite satisfied with what I've created. It's simple, crude and that's the goal. If you want to learn more, I encourage you to visit the repo's site: &lt;a href="https://github.com/grski/braindead" rel="noopener noreferrer"&gt;braindead&lt;/a&gt;. Overall it was a good exercise to create something for fun, something that just works.&lt;/p&gt;

&lt;p&gt;Lastly, I'd like to ask something of you. Please use right tools for the right job. I might be crazy, but I think that going further along on the path of simple sites that take 20 seconds to load and eat up half of your RAM while driving the CPU crazy, is not a good idea. Let's write simple software that runs they way it should. &lt;/p&gt;

</description>
      <category>watercooler</category>
    </item>
    <item>
      <title>A month on tinder in Poland: report with numbers, statistics and graphs</title>
      <dc:creator>Olaf Górski</dc:creator>
      <pubDate>Mon, 28 Nov 2022 12:07:41 +0000</pubDate>
      <link>https://dev.to/grski/a-month-on-tinder-in-poland-report-with-numbers-statistics-and-graphs-e0f</link>
      <guid>https://dev.to/grski/a-month-on-tinder-in-poland-report-with-numbers-statistics-and-graphs-e0f</guid>
      <description>&lt;p&gt;Once upon a time, when you could still go out freely, without worrying for your life, I decided to do a little observation, experiment maybe. Start using Tinder for a month. There are plenty of experiments like that already, but in my home country, Tinder and dating scene are a bit different than in the States or the West in general. Imagine that people here sometimes use Tinder for something other than sex. Interesting, right? Hence, I wanted to experience that by myself and write my own version. Being a software engineer though, I couldn't help myself and had to approach this topic in a way like I would with an engineering problem using more or less a bit of a scientific approach. The result of this experiment are described below.&lt;/p&gt;

&lt;p&gt;This piece was written 2 years ago, things might have changed since then.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some assumptions
&lt;/h2&gt;

&lt;p&gt;First one was to just have certain standards. You can lie to yourself all you like, looks matters. That's it. I could swipe everyone right, but so what? If there's no attraction whatsoever, why waste the other person's time and potentially hurt them? For what? To get laid? No thank you. No offence to anyone, it's just my preference and how I feel. I just know what I like.&lt;/p&gt;

&lt;p&gt;Second of all, I bought Tinder Gold. My time is quite valuable for polish standards and as far as I know, unless you are model-tier man looks-wise, it's way harder to get likes from women. For your average guy, it's at least a 10:1 ratio - for every like you give, you can get at best around 1 like back if you are being lucky that is. I don't know about other countries but that's the case in Poland, with some statistics looking WAY worse. I didn't have that much time every day, therefore I just bought Gold and see who liked me and select pairs from this group. Less effort on my side, which is good.&lt;/p&gt;

&lt;p&gt;Third of all, I try to get a date as fast as I can. Okay, not within first few messages which could be creepy, but writing two weeks just to get rejected is also out of question. So just common sense I'd say.&lt;/p&gt;

&lt;h2&gt;
  
  
  How did I select my pairs?
&lt;/h2&gt;

&lt;p&gt;I gathered a small statistic and from around 25 likes that I got, around half was a no from me. This was influenced by multiple reasons.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Obesity - it's just not my thing, if it's yours - go for it, not for me though, cannot lie here.&lt;/li&gt;
&lt;li&gt;Height above 175 cm. While I personally do not mind if a woman is a similar height or even higher than me (actually this turns me on a lot), as far as I know, it's very hard to attract a woman with such a trait on Tinder. In real life - that's a different story. I just don't want to waste your time.&lt;/li&gt;
&lt;li&gt;Descriptions mentioning FWB deals or just sex. This was about potentially finding a long term partner.&lt;/li&gt;
&lt;li&gt;Descriptions/photos indicating eg. drug abuse or stuff like that. Just your common sense in general. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Overall I think these were not some blown-out requirements I'd say. Just your normal woman, smart one preferably.&lt;/p&gt;

&lt;p&gt;Also about point 1): Here in Poland, we have this joke that a woman ends at 60 kg. Well, I don't agree with this. I don't care that much how much you weight. I care for the confidence you emit and if it just looks good, if it all fits together. Who cares about the scale.&lt;/p&gt;

&lt;p&gt;It might be worth adding that I rather prefer brunettes to blondes. Ya know the type, dark hair, strong eyebrow. Always been my thing, but it didn't mean that I disqualified people not fitting these criteria. Not even once.&lt;/p&gt;

&lt;p&gt;Sometimes I also swiped left only because of the description - if I found something really interesting in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  About me
&lt;/h2&gt;

&lt;p&gt;Enough with the assumptions and requirements. Now, who am I tho?&lt;/p&gt;

&lt;p&gt;First of all: 21, male. I've been working in the IT industry for 3 years already. I make around 50k USD  after-tax per year while living in Poland, where costs of living are considerably lower. Just for reference 50k yearly is around 4x the average in Poland. Overall I think for my age bracket, regardless of the location, I'm doing quite fine. On tinder, I put my position as Product Engineer.&lt;/p&gt;

&lt;p&gt;I have lots of hobbies I'd say. Constantly on the move. Love concerts, sports, gym. I'm interested in art, painting (suck at it tho), sometimes I travel a bit, mostly Europe so far. I love cleaning, cooking a bit, reading and creating stuff by welding, metalworking or writing. Other than that I'm a minimalist and eco freak that loves adrenaline so bungee or parachute jumping is a go for me. I think my life is not boring.&lt;/p&gt;

&lt;p&gt;As far as my observations go, I'm outspoken and have no issue with meeting new people, connecting with them. Being shy is not something that I do. &lt;/p&gt;

&lt;p&gt;Before this experiment, I already was in other relationships so this wasn't my first encounter with women either.&lt;/p&gt;

&lt;p&gt;However arrogant it may sound, for my age, I'm doing damn fine. It just is what it is, describing the facts. &lt;/p&gt;

&lt;p&gt;When it comes to the visual side of things: I'm 178 cm. I don't care tho. I've dated women similar in height, it turns me on as I said previously. If she's taller than you, it just gets a bit tricky kissing, but it's nothing that can't be overcome.&lt;/p&gt;

&lt;p&gt;I'd say my beard is quite nice and thick, same with hair and eyebrows. In the cons: T-level is high so my hairline receded a bit compared to when I was a kid.&lt;/p&gt;

&lt;p&gt;Dark eyes, overall quite dark. In the clubs, I'm often mistaken for Italian/Spanish person. Same happened when I was in Italy. &lt;/p&gt;

&lt;p&gt;The figure is quite okay. Did powerlifting back in the days, so got some muscle here and there, strong back and forearms, but definitely not ripped. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://grski.pl/static/articles/tinder/face.png" rel="noopener noreferrer"&gt;Here are the pics I used&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Some numbers
&lt;/h2&gt;

&lt;p&gt;After a month I had 200+ likes. Out of which I decided to pair with 97 people.&lt;/p&gt;

&lt;p&gt;Not once has a woman written first. It was always me.&lt;/p&gt;

&lt;p&gt;43 didn't even respond. 54 did.&lt;/p&gt;

&lt;p&gt;Out of 54, 31 was out after 3 to 4 messages, in which 19 cases they did not respond in 12 it was me&lt;/p&gt;

&lt;p&gt;Remaining 23 cases - we exchanged at least several massages.&lt;/p&gt;

&lt;p&gt;17 girls just stopped responding, out of nowhere.&lt;/p&gt;

&lt;p&gt;2 times I had to unmatch coz. 1st one was serious about marriage and visibly crazy, withing first massages, the second one was very very very beautiful and my type 100%, but damn, my back hurt so much from carrying the whole conversation.&lt;/p&gt;

&lt;p&gt;Out of these, I got 4 dates. 1 stopped responding when it was the day of our meeting.&lt;/p&gt;

&lt;p&gt;3 showed up. In 2 cases it was nice and shiny, in 1 case it didn't as she was just a different person in real life.&lt;/p&gt;

&lt;p&gt;This graphic sums it up:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgcosv3pqkkn279r3mv0w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgcosv3pqkkn279r3mv0w.png" alt="Statistics of my Tinder matches" width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The conversion rate of about ~3%. A bit similar to business/sales. Anyway. I got to know some interesting people out of this, which makes it worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Insights
&lt;/h2&gt;

&lt;p&gt;First of all, a lot of these talks are daaaaaamn hard. You had to carry the conversation until you found a spot to hook them in. Something that gets them talking, some interests of theirs or something. After that, it's all easier.&lt;/p&gt;

&lt;p&gt;Some percentage of matches wasn't that kind/nice at the beginning. Trying to shit on your head and see if it works, if you let them. After a proper rebuttal or an equally rude response, they always went back to being nice.&lt;/p&gt;

&lt;p&gt;The description doesn't change the number of likes I get, it changes slightly the type of girls who give the likes. &lt;/p&gt;

&lt;p&gt;Overall I think I'm slightly above average looks-wise. Some of the women I know have complimented me honestly that I'm handsome and it wasn't my grandmother.&lt;/p&gt;

&lt;p&gt;When you take this all into account... A question arises? How does the situation look like for average or below average guys on Tinder in Poland? I don't want to know, but honestly, it kinda makes me understand a bit, how frustrated some men must feel and why they hate so much on tinder.&lt;/p&gt;

&lt;p&gt;Is it good or is all of this bad? I won't answer this for you - it's your choice. I just describe facts and numbers here.&lt;/p&gt;

&lt;p&gt;Is Tinder good or bad? Again - answer it yourself. For me, it's just suboptimal. 3 dates for a month of using it. I can get 3 dates by going out to a dancing/night club once. In real life, my conversion rate is way higher. Gestures, body posture, smell, voice, it all comes into play in real life. On tinder, you are just your photos and a text. That's it. &lt;/p&gt;

&lt;p&gt;I'm also not sure if I like this culutre. Couple of seconds, quick judge, beng. Next one. A whole person simplified to one photo. Well, it is what it is I guess.&lt;/p&gt;

&lt;p&gt;In my opinion, if you are just a shy, average polish guy, your experience with Tinder might not be pleasant.&lt;/p&gt;

&lt;p&gt;Again - I don't say any of this is inherently good or bad. I just say what works better for me and describe a certain trend. What to do with it further - that's up to you to decide.&lt;/p&gt;

&lt;p&gt;Also, before I finish - a little disclaimer for all of you who are foreigners. These numbers do not have any reflection on how Tinder wll look for you in Poland. &lt;br&gt;
In a recent study that I read somewhere, Polish women were among top 3 in UE who are most friendly to men outside their country. What does that mean? You have it easier.&lt;/p&gt;

&lt;p&gt;That's it for today folks.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>resources</category>
    </item>
    <item>
      <title>Facilitating retrospective for the first time — report</title>
      <dc:creator>Olaf Górski</dc:creator>
      <pubDate>Mon, 28 Nov 2022 12:02:32 +0000</pubDate>
      <link>https://dev.to/grski/facilitating-retrospective-for-the-first-time-report-eam</link>
      <guid>https://dev.to/grski/facilitating-retrospective-for-the-first-time-report-eam</guid>
      <description>&lt;p&gt;Recently I had the opportunity to facilitate a retrospective for the first time in my life, this will be my summary of the experience.&lt;/p&gt;

&lt;p&gt;The idea began with one of our founders asking me if I'd like to do that.&lt;/p&gt;

&lt;p&gt;Originally posted couple of years ago as &lt;a href="https://grski.pl/retrospective.html" rel="noopener noreferrer"&gt;Facilitating retrospective for the first time - report&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Me? Agile/scrum-slavery/whatever-sceptic, leading a retrospective? Sounds like a recipe for a disaster.&lt;/p&gt;

&lt;p&gt;Well, I agreed immidietely. Despite the fact that for the most part, I'm not a huge fan of all the current buzz around agile, scrum slaves (I love this term and abuse it often) and so on, plus the one pleasant thing that comes to my mind when I hear retrospective is just this song. Seriously, it's either just me or some people have gone mad, coming up with more extreme and weird practices when it comes to ~AGILE~, that produce little to no results other than annoying the developers and wasting business people's time.&lt;/p&gt;

&lt;p&gt;Anyway - seeing lots of such hype, I get sceptical instantly. Hence I decided to look what's behind the other side of the fence.&lt;/p&gt;

&lt;p&gt;Of course, I was aware that there are lots of worthwhile practices, things, habits and thinking patterns to learn from Agile bunch, it's not like I'm saying that all agile is useless. Do not interpret my words as such. It's just that some people are doing crazy stuff that is not Agile and calling it Agile. Especially in Poland so it seems. And that spirals into madness at times. I've seen it happen, no psychotherapist helped. I digress.&lt;/p&gt;

&lt;p&gt;As it was my first time, I got offered some help. The company got me some hours with our friendly agile coach that helps us with building certain processes within the company. We had a quick chat before I planned my retrospective.&lt;/p&gt;

&lt;p&gt;I've learned a bit about setting the stage, gathering the data, insights, nice tools to use and so on. This quick session, despite my initial doubt about it, helped me quite a bit. So thanks a lot here and shout out to Marcin Konkel!&lt;/p&gt;

&lt;h2&gt;
  
  
  Ready, set, go
&lt;/h2&gt;

&lt;p&gt;So came the day and time. The meeting started. I got nervous once we were all there. If it wasn't enough that it was my first retrospective, because of the way we work, and we work 100% remotely, it was a remote one. I personally find it harder to communicate remotely - you need to be more precise, exact, more communicative and attentive. It takes some time and skill to get used to this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set the stage
&lt;/h2&gt;

&lt;p&gt;Before we began, I decided to set up some clear rules and say out loud my assumptions, which were: We do not interrupt each other when talking. Considering how we hold the retrospective remotely, we need to adhere to this even more than ever. If you have anything to add outside of your turn/speaking time, raise your hand to the camera. The speaker or facilitator should take notice and give you the occasion. The first person to speak in a given round for a given exercise is chosen by the facilitator. That person gets some time to answer the question while others listen. After you are done speaking, you point the next person who should speak. After we agreed on these rules, I mentioned one thing.&lt;/p&gt;

&lt;p&gt;It was a retrospective for our team. I'm a part of that team, actively participating in the development and work. Today though I was a facilitator. I was nothing more. Therefore I clearly stated, that today I'm doing just that - facilitating. Monitor, manage, facilitate and moderate the talk. Let them do the rest.&lt;/p&gt;

&lt;p&gt;Also, I'm a bit of a direct guy - instead of using any fancy tools, I just went with talking. No post-it notes. No whiteboards. Just our faces, voices and me, quietly noting the whole retro in the background. Yeah - scary you actually have to talk to people. Ugh.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warm-up
&lt;/h2&gt;

&lt;p&gt;I at first wanted to lighten the mood a bit, get people talking, as I was perfectly aware of what they were feeling at the moment. Some of them got interrupted mid-work, got thrown out of the flow to attend this meeting. They're in another context right now, so it's time to bring them here, into the moment.&lt;/p&gt;

&lt;p&gt;How to do that? I asked everyone a simple question.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What, be it in your private or work life, has recently made you happy?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This allowed the team to get a new little glimpse into each other. Maybe finding a new way of looking at each other. I know it sounds simple, but that's what actually happened. Everyone surprisingly shared some happy little moments from their private lives and you know what? I felt like it kind of tightened the bond that we all share, as a team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Past retrospective
&lt;/h2&gt;

&lt;p&gt;Before this one, we held another retrospective in the past with a different agile coach, which resulted in some goals declared. Now it was the time to check upon them. So I've asked each person that had a goal defined, about the status of their goal, how is it going and what's their opinion, experience so far.&lt;/p&gt;

&lt;p&gt;All the people agreed that previous goals were necessary, they are done or in progress and they improved our work somehow. Nice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Goodies - gathering the data
&lt;/h2&gt;

&lt;p&gt;We work in a very multicultural and international team, yet despite that, it somehow turned out that on this retrospective, only Polish teammates were present. Being Polish myself, I know about our quirks, one of which is complaining. It's like hyperinflation, once it starts, there's no going back.&lt;/p&gt;

&lt;p&gt;To set a proper vibe, I decided to first talk about some good things, to get people into a good mood. I've asked them all a question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What are some things that you think we do that make us deliver faster, better and overall contribute to us doing a better job or what are you pleased with recently?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here everyone shared their perspective, insights and so on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Baddies - gathering the data
&lt;/h2&gt;

&lt;p&gt;So, while they were still in a good mood and the aura was positive, I dropped in the bomb, that sometimes opens the gates of hell on Polish retrospectives - what went bad? Let's get down to the business. Enough of the sugar coating. The Polish way. Onion way.&lt;/p&gt;

&lt;p&gt;Lo and behold, the complaining started. Or so I thought - that it'd start. Instead, it didn't.&lt;/p&gt;

&lt;p&gt;I asked a question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What do you think we did wrong recently? What have you observed that makes us deliver less, slower? What makes your work less enjoyable?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of simply spewing mindless blabber, the team did something nice. They came up with constructive and clever observations, insights and comments. It was quite amazing to witness honestly. Somehow I'm surprised at times how clever the people I work with are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Happies - still gathering the data
&lt;/h2&gt;

&lt;p&gt;As I like to change the moods the way sinusoid function changes its value, I changed the topic again.&lt;/p&gt;

&lt;p&gt;This time I decided to facilitate talk about things that we do that make them happy. It was different from the first question - good things we do - as here it wasn't about delivering, productivity and value generation. It was all about human connection and how we behave towards each other, how we interact. I thought it would be nice if they told each other what they notice about each others behaviour and what they appreciate.&lt;/p&gt;

&lt;p&gt;I don't know about you, but I like a pat on the back sometimes - a sign that someone appreciates my efforts.&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What do you appreciate that we do? Not in the aspect of productivity, coding or technicalities, but just like human beings - what makes you want to stay with us and spend our time together?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And this, oh boy, this point was quite sweet honestly. So, if we had sweet, then time for bitter again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop, it hurts
&lt;/h2&gt;

&lt;p&gt;Now I decided to make the waters murky a bit. Threw in a topic:&lt;/p&gt;

&lt;p&gt;Goals to improve. What should we stop doing? What should we achieve to improve?&lt;/p&gt;

&lt;p&gt;And here I decided that this retrospective shouldn't end with just empty talk. I mean yeah sure, we improved the bonds a bit, got to know each other a tiny bit better and had a blast talking to each other (really!), but it's not enough for me.&lt;/p&gt;

&lt;p&gt;So I threw in an idea.&lt;/p&gt;

&lt;p&gt;Anyone, if they had such a wish, could choose one or more tasks, voluntarily, pain points of theirs, that they want to resolve till the next retrospective.&lt;/p&gt;

&lt;p&gt;Other than that, they had to also choose a &lt;del&gt;witcher&lt;/del&gt;, watcher for their tasks. Maybe owner would be a better word here. That person will be responsible for checking up on their progress and so on. Basically, someone to make sure that the job will get done, other than that person. Someone to nag them a bit, like every other day, just ask a famous, in our company at least, question - What's the status here?&lt;/p&gt;

&lt;p&gt;Why? While I trust my teammates that they will deliver project related things fully, these tasks were in the category of nice to do and all, but we won't die if we don't do them. They weren't critical, just a nice to do. Hence it'd be good to have some accountability and additional motivation to do them.&lt;/p&gt;

&lt;p&gt;It also went well - almost everyone decided on some tasks for themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inception
&lt;/h2&gt;

&lt;p&gt;After that, there came a time for small inception. Retrospective for the retrospective. Basically, a short round of feedback regarding today's retrospective, my facilitation, value generated and so on. Supposedly they liked it, or that's what she said. Yeah.&lt;/p&gt;

&lt;p&gt;That'd be it I guess. After that, the retrospective came to an end. I survived and actually found it meaningful. Yet quite challenging on the other hand.&lt;/p&gt;

&lt;p&gt;I'm young, got this problem with my ego you know. It was a bit hard being at the sidelines, just spectating most of the time, being the moderator, instead of an active participant. I had some moments where I wanted to ditch my role as a facilitator and engage as a team memeber starting my endless blabbering, throwing in my two cents, but I refrained from doing so, managing to stay in my role.&lt;/p&gt;

&lt;p&gt;It was a bit humbling and rewarding honestly.&lt;/p&gt;

&lt;p&gt;Just so you know, later these tasks that we defined - they got done actually. So it, in fact, didn't end with just empty talking. Supposedly this idea with owners/watchers helped a bit too - maybe you can use it? No idea.&lt;/p&gt;

&lt;p&gt;So well, Agile processes can be nice and useful too, that's first. Second, show some empathy and check out the other side of the fence. Maybe there's something valuable there, you just don't know it. Lastly, get out of your comfort zone. You'll grow.&lt;/p&gt;

&lt;p&gt;If you are preparing for your first retro too, maybe you'll find this article helpful. Also: protip. Take notes. I did. Then, after the retro, I've turned it into a nice confluence page and tasks on Jira.&lt;/p&gt;

&lt;p&gt;That'd be it for today, I, Olaf, was your humble host, hope you enjoyed.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>resources</category>
    </item>
    <item>
      <title>Why f-strings are awesome: performance of different string concatenation methods in Python</title>
      <dc:creator>Olaf Górski</dc:creator>
      <pubDate>Mon, 28 Nov 2022 12:00:39 +0000</pubDate>
      <link>https://dev.to/grski/performance-of-different-string-concatenation-methods-in-python-why-f-strings-are-awesome-2e97</link>
      <guid>https://dev.to/grski/performance-of-different-string-concatenation-methods-in-python-why-f-strings-are-awesome-2e97</guid>
      <description>&lt;p&gt;Hi there, today I'd like to write a few things about f-strings in #python, why I think they're awesome, why we should use them and why they'll save the world from World War III.&lt;/p&gt;

&lt;p&gt;Originally published as &lt;a href="https://grski.pl/fstrings-performance.html" rel="noopener noreferrer"&gt;performance of string concatenation in Python.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Personally speaking, I'm a big proponent of f-strings. I sue them when I can where I can and tell people to do the same. Elegant, readable and simple in usage. I got curious though - I mean, nothing in life comes free, right? This elegance, simplicity and just pure beauty must come at a price. In Python, most of the time, the price we pay for different things is performance. Well, I've decided to check if that's the case with f-strings.&lt;/p&gt;

&lt;p&gt;So today we will compare few ways you can modify/add strings together or generally format them.&lt;/p&gt;

&lt;p&gt;I'll compare f-strings, string concatenation, join() method, format() method and template string.&lt;/p&gt;

&lt;p&gt;No % operator for string in this comparison. Why? It's old. It's a bit ugly. My personal preference - I just don't like it. It reminds me of C way too much and Python syntax ain't no C. We can do better than %.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we will test
&lt;/h2&gt;

&lt;p&gt;To test and measure the performance of our code we will use timeit module that's a part of Python standard lib, calling Python from a command line. All the variables that we will use, will be defined in a command outside of the measuring time as to reduce the amount of overhead we have and the noise that Python itself generates - we are just interested in the string manipulation functions, nothing more. We will run standard 1000000 iterations in each loop, with 3 loops for each command. From all of these the program will output the shorter time it took to finish a given operation. Now let's get to the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&gt;

&lt;p&gt;So, watch and behold my fugly code that I've produced on my knee, the one below (as if other code I produce was any different though…), that'll measure everything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 -m timeit -s "x = 'f'; y = 'z'" "f'{x} {y}'"  # f-string
python3 -m timeit -s "x = 'f'; y = 'z'" "x + ' ' + y"  # concatenation
python3 -m timeit -s "x = 'f'; y = 'z'" "' '.join((x,y))"  # join
python3 -m timeit -s "x = 'f'; y = 'z'; t = ' '.join" "t((x,y))"  # join2
python3 -m timeit -s "x = 'f'; y = 'z'" "'{} {}'.format(x,y)"  # format
python3 -m timeit -s "x = 'f'; y = 'z'; t = '{} {}'.format" "t(x,y)"  # format2
python3 -m timeit -s "from string import Template; x = 'f'; y = 'z'" "Template('$x $y').substitute(x=x, y=y)"  # template string
python3 -m timeit -s "from string import Template; x = 'f'; y = 'z'; t = Template('$x $y')" "t.substitute(x=x, y=y)"  # template string2
python3 -m timeit -s "from string import Template; x = 'f'; y = 'z'; t = Template('$x $y').substitute" "t(x=x, y=y)"  # template string3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks basic and crude, but it'll do the job. For Template string I've considered three cases. First one is when the initialization of the instance of the Template class happens during the time that counts towards the result, the second one is where the initialization is done before the timer is started, I initialize the instance and pass it over and the third one is where I initialize the instance and also access the proper method already, so that the only thing that will contribute to the outcome's time, will be the method. What do I mean by that? Well, basically, as you know, instantiating a class takes time and memory in Python, well in other language too, actually. So considering a case where this takes place in a different part of the code is a sane thing to do. That much is obvious.&lt;/p&gt;

&lt;p&gt;But why do I also test a case, in which I pass instance.attribute to the test loop, instead of calling instance.attribute in the loop itself? Well, it's quite interesting nifty thing. Python, in the background, when you create a class, creates a dictionary that contains a mapping with all the attributes/method names and so on that you have on a class and that you can access using the . dot operator. So each time you you use it, in the background, a dictionary lookup happens. Of course this changes a bit if you use &lt;code&gt;__slots__&lt;/code&gt;, but we won't be discussing this case here. If the key is found -&amp;gt; proper attr/method is returned, otherwise we get attribute error. Anyway. All of this costs time. Even though lookup, for the most part and in most cases, in dictionaries is O(1) operation, then well, it still means additional steps. So just for the sake of it, we will see what's the difference with and without that operation by testing different cases.&lt;/p&gt;

&lt;p&gt;Same thing for join &amp;amp; format. Here I've considered two cases for each - one with &lt;code&gt;.&lt;/code&gt; access and one without - just calling a function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Here are the results.
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;f-string: 10000000 loops, best of 3: 0.0791 usec per loop
concat: 10000000 loops, best of 3: 0.0985 usec per loop
join , no lookup: 10000000 loops, best of 3: 0.112 usec per loop
join: 10000000 loops, best of 3: 0.144 usec per loop
format, no lookup: 1000000 loops, best of 3: 0.232 usec per loop
format: 1000000 loops, best of 3: 0.264 usec per loop
template string3: 1000000 loops, best of 3: 1.01 usec per loop
template string2 loops, best of 3: 1.06 usec per loop
template string: 1000000 loops, best of 3: 1.36 usec per loop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Surprise, surprise!
&lt;/h2&gt;

&lt;p&gt;Honestly, I did not expect that f-string will be both the most elegant solution and also the fastest one! This smears joy over my heart, makes me want to live. The second place went to concatenation and so on - you can basically see for yourself.&lt;/p&gt;

&lt;p&gt;Considering that the optimization I made - getting rid of the dot operation and attr lookup in the test loop, is rather unpractical and should not be used unless you want to end up in seventh circle of hell, together with Java creators, I'll remove it from the rankings - just wanted to show it for the sake of it. So here's the ranking for a simple case of manipulating three strings.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;f-string&lt;/li&gt;
&lt;li&gt;concatenation&lt;/li&gt;
&lt;li&gt;join()&lt;/li&gt;
&lt;li&gt;format()&lt;/li&gt;
&lt;li&gt;Template-string&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let the joker dance
&lt;/h2&gt;

&lt;p&gt;I've showed you a simple example with three strings and their manipulation. What about a case when someone wants to manipulate more of them?! Like, idk, maybe 13 at a time? For example you want to create a string containing 13 letters separated by a space. WHY NOT? Let's do it, Morty.&lt;/p&gt;

&lt;p&gt;Also take note, here I won't test the variants with dot operator and lookup and without them. Why? Because as the amount of variables increases, the lookup itself becomes smaller and smaller part of the result, in this case it practically does not matter, so why bother.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 -m timeit -s "a, b, c, d, e, f, g, h, i, j, k, l, m = [str(s) for s in range(13)]" "f'{a} {b} {c} {d} {e} {f} {g} {h} {i} {j} {k} {l} {m}'"  # f-string
python3 -m timeit -s "a, b, c, d, e, f, g, h, i, j, k, l, m = [str(s) for s in range(13)]" "a + ' ' + b + ' ' + c + ' ' + d + ' ' + e + ' ' + f + ' ' + g + ' ' + h + ' ' + i + ' ' + j + ' ' + k + ' ' + l + ' ' + m"  # concat
python3 -m timeit -s "t = [str(i) for i in range(13)]" "' '.join(t)"  # join
python3 -m timeit -s "t = [str(s) for s in range(13)]" "'{} {} {} {} {} {} {} {} {} {} {} {} {}'.format(*t)"  # format
python3 -m timeit -s "from string import Template; a, b, c, d, e, f, g, h, i, j, k, l, m = [str(s) for s in range(13)]" "Template('$a $b $c $d $e $f $g $h $i $j $k $l $m').substitute(a=a, b=b, c=c, d=d, e=e, f=f, g=g, h=h, i=i, j=j, k=k, l=l, m=m)"  # template string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I wonder how the results will turn out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;join: 1000000 loops, best of 3: 0.217 usec per loop
f string: 1000000 loops, best of 3: 0.399 usec per loop
format: 1000000 loops, best of 3: 0.811 usec per loop
concat: 1000000 loops, best of 3: 1.13 usec per loop
template string: 100000 loops, best of 3: 2.04 usec per loop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some changes there. A lot of them actually, but basing on previous results, I'm not that surprised. Why?&lt;/p&gt;

&lt;p&gt;Well, let's begin with what changed. Join on the 1st place, from the 3rd, concat on second to last from it's glorious 2nd place, format to 3rd from the 4th. Template-string last, as always. Quite reasonable outcome.&lt;/p&gt;

&lt;p&gt;First place, so our winner in this case, join is obvious case of optimization. Just look at what we are doing here - nothing more than just joining some strings with a common separator and that's it. Wasn't join precisely designed to do just that? I'm sure, and CPython source code shows this, that there was some heavy optimizing made on this method as it's part of the core of the language, be it on the implementation level or maybe even on the CPython level. It allows join to handle an arbitrary (to a point) large number of args while still being quite performant. Which is nice. Why? Because again - the most elegant solution (f-string was not as elegant in this particular case) turned out the fastest.&lt;/p&gt;

&lt;p&gt;Second place, f-strings. Not surprised anymore. After the initial result of f-strings being first and me being surprised, I dug a bit deeper. Turns out that at first, in the first implementation, f-strings very indeed very slow. It was because they were, internally, translated to nothing more than a bunch of joins/formats, don't remember exactly. Only later did they create a special OPCODE on the CPython level, specifically designed for f-strings, which made it way faster as it allowed the core developers to make optimizations on C-level code.&lt;/p&gt;

&lt;p&gt;Why did format appear before concatenation? Let me tell you. It's all about evaluation and strings being immutable in Python. Each time we do anything to them, new one is created and returned. In order to do that, Python must allocate new memory for the new string with appropriate length, copy the contents of the strings. Allocation and copy, this sounds like something that might take some time. In case of our code, this takes place each time we call the + operator. So basically it means, that when we typed in &lt;code&gt;a + ' ' + b + ' ' + …&lt;/code&gt; and so on, underneath Python called in something like that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Allocate memory that'll fit a and ' '&lt;/li&gt;
&lt;li&gt;Copy a to that place or temp variable&lt;/li&gt;
&lt;li&gt;Copy ' ' to that place or temp variable &lt;/li&gt;
&lt;li&gt;Resulting string you ought to add to b&lt;/li&gt;
&lt;li&gt;Allocate memory that'll fit the resulting string and b&lt;/li&gt;
&lt;li&gt;Copy ….&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You get the idea. All of this takes time at least the memory allocation process that in this case needs to happen at least N times for N added strings. &lt;/p&gt;

&lt;p&gt;Last one is obviously our Trajan horse - template string which is the slowest and should be avoided unless for some reason desirable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;

&lt;p&gt;First iteration that comes to mind but is a bit less Pythonic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt; 
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enduser&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;arPfEu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    
    &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEMUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enduser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pfeu_email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        
        &lt;span class="n"&gt;strEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gettext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;* The email {user_name} is already in X&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enduser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pfeu_email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        
        &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;strEmail&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;br/&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Can be improved to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enduser&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;arPfEu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEMUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enduser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pfeu_email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;strEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gettext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;* The email {user_name} is already in X DB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enduser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pfeu_email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;strEmail&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;br/&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strEmail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="err"&gt; &lt;/span&gt;
&lt;span class="n"&gt;final_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which then boils down to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;final_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="nf"&gt;gettext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;* The email {user_name} is already in X DB &amp;lt;br/&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;end_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pfeu_email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_user&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;arPfEu&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEMUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pfeu_email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In Python, most of the time, mechanisms that look elegant in a particular situation, are almost for sure optimized for it, that's why you ought to use them. I love this snake. Elegant code that also is the fastest option available most of the time. Nice. If there are a lot of strings you ought to join in a predictable manner - use join. In most of the other cases, use f-strings where you can, enjoy your life.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
    </item>
    <item>
      <title>How to write README files like a pro</title>
      <dc:creator>Olaf Górski</dc:creator>
      <pubDate>Mon, 28 Nov 2022 07:49:44 +0000</pubDate>
      <link>https://dev.to/grski/readme-file-structure-and-sections-pm8</link>
      <guid>https://dev.to/grski/readme-file-structure-and-sections-pm8</guid>
      <description>&lt;p&gt;There is a profound need for a proper README file in every project one works on. It's where very basic but key information regarding the projects should be contained, but not only that. A good README is made by having a bit more than basic project information. What exactly? We will go into that in this document.&lt;/p&gt;

&lt;p&gt;This post's target audience are beginners who have yet to believe in the power of documentation. Also my experiences here are mostly relevant to my work - so web based apps, depending on the app this will vary, however I think the advice below is a good start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technology
&lt;/h2&gt;

&lt;p&gt;Usually the README we work with have &lt;code&gt;.md&lt;/code&gt; extrension, the marking of a Markdown file. Markdown is something that let's us format text in a certain way. We will stick with that and assume Markdown is used in the README file for convenience's sake. Make note though, it does not have to be the case. &lt;/p&gt;

&lt;h2&gt;
  
  
  Sections
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Title
&lt;/h3&gt;

&lt;p&gt;Start your README with a line of a project name, like this: &lt;code&gt;# Example Project&lt;/code&gt;. It's obvious but needed. &lt;code&gt;#&lt;/code&gt; marks a given line as a header in Markdown.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;Here you must put basic information about the project. &lt;br&gt;
What system component it is? For example - API, Worker, frontend, library. &lt;br&gt;
What functionalities it includes? For example - This is invoicing API for foobar system.&lt;br&gt;
Some additional context - Who the stakeholders are (for who is the project intended), what the business need/idea is behind this application, who are the actors, end users, few words about project from business perspective.&lt;br&gt;
The summary should also contain and use wording that is closely tied to the Domain of the problem/need that the project is solving. &lt;/p&gt;

&lt;h3&gt;
  
  
  Tech stack
&lt;/h3&gt;

&lt;p&gt;This section should contain the most important packages used to build the application, 3rd party dependencies, services etc. &lt;/p&gt;

&lt;p&gt;This will enable the reader to gain a quick understanding of application dependencies and the technology used, problems solved.&lt;/p&gt;

&lt;p&gt;One should not forget to link the used technologies to proper websites, resources to ensure no ambiguity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local development setup
&lt;/h3&gt;

&lt;p&gt;Write down a list of steps needed to bring the project up for development locally, on the developer's machine. Other than that you should also add a list of commonly executed operations like cleaning the databse; (re-)setup the project from scratch; migrate the database. Same with project-specific knowledge - this section is a good place for such things. For example - handling l10n, i18n with a 3rd party service like PhraseApp or OneSky.&lt;/p&gt;

&lt;p&gt;Local development setup section should be prepared while having less technical people in mind, as stakeholders which not always are tech-savvy, might be using it. Additionally Local Development setup should be as automated as possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment
&lt;/h3&gt;

&lt;p&gt;Briefly describe how the application is deployed, where to look for more detailed explanation if needed, what is the deploy setup or a few words about CI/CD. &lt;/p&gt;

&lt;h3&gt;
  
  
  Contributors
&lt;/h3&gt;

&lt;p&gt;Leave some kind of list of people who contributed to the project or are key people in the context of this project. This can be useful when jumping from one project to another after some time, as it allows one to identify the context holders very quickly. &lt;/p&gt;

&lt;p&gt;Aaand that's it.&lt;/p&gt;

&lt;p&gt;Check out my other articles:&lt;br&gt;
&lt;a href="https://dev.to/grski/three-types-of-managers-which-one-are-you-5197"&gt;Three types of managers - which one are you?&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/grski/tenancy-pattern-in-a-saas-product-2ea0"&gt;Tenancy pattern in a SaaS product&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/grski/how-i-created-my-own-blogging-system-in-less-than-100-lines-of-code-305b"&gt;How I created my own blogging system in less than 100 lines of code&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/grski/pyenv-poetry-and-other-rascals-4g5l"&gt;Pyenv, poetry and other rascals - modern python dependency and version management&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>beginners</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Three types of managers - which one are you?</title>
      <dc:creator>Olaf Górski</dc:creator>
      <pubDate>Fri, 21 Oct 2022 11:48:46 +0000</pubDate>
      <link>https://dev.to/grski/three-types-of-managers-which-one-are-you-5197</link>
      <guid>https://dev.to/grski/three-types-of-managers-which-one-are-you-5197</guid>
      <description>&lt;p&gt;Good manager/team leader is something that can make or break a team. Teams are what builds a company. If managers can make or break a team, they can make or break a company. How they can make or break a team? By breaking what teams consist of. People. This piece will be about that - about people, and managerial approach to them, or rather, the road that I see lots of managers go through and how it makes them change.&lt;/p&gt;

&lt;p&gt;Note: you can also read this post on my &lt;a href="https://grski.pl/managers.html"&gt;personal blog. &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During my career, I've come upon multiple managers. Some were awesome, some changed my life in a meaningful way, some were utterly incompetent and stupid some ended up being like a family to me, almost. After some time I've started discovering a certain pattern that allowed me to isolate three main "levels" of managers. Today I'll write a bit about them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level one: Masters of Excel
&lt;/h2&gt;

&lt;p&gt;this one I see often with freshly baked aspiring ones. The new hotshot thinking they can top the world with nothing more than just their will alone. They love their excel sheets, they love to optimise, sometimes micromanage and so on. It's that time where you get a bit drunk on your newfound power.&lt;/p&gt;

&lt;p&gt;These people see one thing: numbers or dollars. I've met them far too often. They're the most dangerous thing that can happen to a company. Seemingly they are competent, you can't find any stain on their work at the first glance. The groundwork is there, the plan is there, the numbers are there. Yet nothing is going as it should.&lt;/p&gt;

&lt;p&gt;Why are they the most dangerous kind? Because of the facade that they impose - they aren't the ones which you'd usually suspect as the root of the problem, but once you get to know their kind, you just know and then can spot them quite often. How? Just find the most miserable team in the company. The quiet team, the one that fails quietly, for some elusive reason. Don't look for a team that complains. They still have hope left. Look for something else.&lt;/p&gt;

&lt;p&gt;Teams that complain - they still have the will to do anything, to at least, complain. The real defeat and resignation happen when there's nothing, when people are quiet, when they are so fed up with everything, that they'd just rather remain quiet and pretend everything is good, but even on the outside, on a closer look, you can see it's not. When enemies argue about something or bicker one another, they aren't fighting, they are talking in a way, there's some form of communication happening. The real fighting happens when any form of communication ceases and actions follow. Deadlines are not met, projects are failing. Often the rotation of teammates within the team is also quite high. People either get stuck in such a team out of resignation and just remain there for a very long time, rotting in pits of despair, or they run away immediately. Usually, at the top of such a team, you'll find what I call Level one manager - The Master of Excel.&lt;/p&gt;

&lt;p&gt;Masters of Excel see only numbers or dollars. They never admit to a mistake, they never think they've made one - it's always someone or the team. They don't see people, they don't see teammates, they see resources. Resources that can be used, exploited even, that can be "optimised". There's nothing more involved. Like simple maths. If you have a task for 80h, it'll take about two weeks of one developer's work, if you put two developers then, it'll happen in a week, right? Everyone always performs at a very constant pace, no matter the context and situation, right? Private life doesn't affect professional life at all, right? Margin and profits are all that matters, right? Do these numbers look good? If yes, everything is going as it should, right? RIGHT?! Technically, yes. Practically, no.&lt;/p&gt;

&lt;p&gt;The manager such as this break the spirit. They break people that make the team.&lt;/p&gt;

&lt;p&gt;Without people, you have no team. Without people, your product will not launch. Without people, deadlines won't be met, but they don't see that. Resources are just that. Resources. In their heads at least.&lt;/p&gt;

&lt;p&gt;This is outright mad and destroying. There's nothing worse than feeling a lack of appreciation, than feeling like you are just a cog in a big fuckin machine that can be exchanged with one swipe of the manager's hand. Let's not cheat ourselves, in reality, that's the case sometimes I mean every one of us isn't THAT special, or at least partly, but damn, leaders and managers are there to make that feeling go away. At least partly.&lt;/p&gt;

&lt;p&gt;I've met with many examples of such people. They all have one thing in common. Mayhem they produce.&lt;/p&gt;

&lt;p&gt;Level ones can deliver very very short-lived projects at best. Projects where people have no chance to get to know each other, to be meet with hardships and real challenges. I'd say a couple of weeks at most. It's possible with level one because in such a short time frame they are not able to destroy their team members enough. They cannot harass them into oblivion of despair unless they are very talented at it, that is.&lt;/p&gt;

&lt;p&gt;Usually, these people either remain stuck in this mindset for a looooong time, or they will try out different advice to maybe improve or fix the team. Because they often see no fault in their behaviour. Anyway.&lt;/p&gt;

&lt;p&gt;This usually leads to them learning about empathy. Which brings us to...&lt;/p&gt;

&lt;h2&gt;
  
  
  Level two: Empathetic Excel Acolytes
&lt;/h2&gt;

&lt;p&gt;Here things get a little bit interesting I'd say. The person that has reached this level is in an interesting place. Usually, fueled by constant failure and drive to improve their sheet, team's performance and other VERY IMPORTANT CORPORATE KPIs, they learn about empathy. Oh, yes! EMPATHY! That thing we should promote, so people feel better, feel more connected to the team and company so they perform better? Sounds good, right?&lt;/p&gt;

&lt;p&gt;Fuck no. This is the problem with level twos. They see empathy only as a tool, a tool to get the team to work more. Or to perform better. Either way, this is not real empathy in any way shape or form. This is just calculated manipulation in reality.&lt;/p&gt;

&lt;p&gt;Don't take me the wrong way - true leader SHOULD and HAS to know a little bit of manipulation, it should be natural to them, but they shouldn't use it. Good managers should be able to read people and their emotions, yet never use it against them, never manipulate their team into doing things. This doesn't work in the long run but can bring in short-term benefits sometimes. Which makes no sense.&lt;/p&gt;

&lt;p&gt;This skill should be used for compassion, knowing when to back out, cut a team member little slack sometimes, for various reasons that you observed.&lt;/p&gt;

&lt;p&gt;So while level two manager is a bit better than level one, they're still bad, at least they try and pretend. It's something right? What kind of pretending and what kind of fake empathy I'm talking about here?&lt;/p&gt;

&lt;p&gt;Oh, hi Susan. Good job at the Empathy Workshop with our Agile coach last week. It's very important to promote empathy at our company! Your mother died yesterday so you'd like to leave early today? Oh, not that kind of empathy. Maybe another workshop with an Agile/Empathy coach will help you to get this better? The company will pay for it! Think about it Susan and keep it up, champ!&lt;/p&gt;

&lt;p&gt;Something like that I'd say. It's kind of hard to describe, but you can feel it and spot it most of the time. It's when you feel like a cattle, that's being taken care of by a not-so-great owner, that doesn't love his animals, he just does the bare minimum so they have the meat of acceptable quality. Yeah, something like that.&lt;/p&gt;

&lt;p&gt;That's level two. While still better than level one, he's also a danger, just a smaller one. This one is a bit harder to spot on the surface, but after talking with them a bit, you also can get it.&lt;/p&gt;

&lt;p&gt;Level two can actually deliver some projects. at best the short to medium-length ones. A couple of months maybe, probably half a year at best. In this period it's still possible to maintain the facade, to fake it, so the team might still be okay, they'll even perform nicely. But it's a trap once you get a longer project, where people really get to know each other or have worked with each other before. This is where they ultimately fail. Sometimes crushingly. There are not so many things worse than the feeling when you get once you realise your manager/leader was a leader two. That you were only cared for so that you perform better, you were manipulated. Resentment follows bitterness, feeling of betrayal.&lt;/p&gt;

&lt;p&gt;Back to the topic - long term projects. Now, this is where the real challenge and fun beings and with it enters...&lt;/p&gt;

&lt;h2&gt;
  
  
  Level three: Fractured But Whole
&lt;/h2&gt;

&lt;p&gt;Usually, level three is a level one or level two that has just failed enough times, that was broken enough times and mended together by the passage of time and self-reflection. Rarely it's the genius type that's just born to be a leader/manager.&lt;/p&gt;

&lt;p&gt;Most of the time level threes have experienced failure and utter, crushing defeat many times in their life. They are aware that we are just humans and nothing more. And humans, on some levels, are extremely tough and durable, yet on some times, they can be so brittle and fragile that even the tiniest thing can make them shatter into oblivion. How do they know? Because they've been there.&lt;/p&gt;

&lt;p&gt;They also know that even if you are fractured, you can be whole again. Even if you shatter you can be rebuilt. Same applies to the team.&lt;/p&gt;

&lt;p&gt;These are the managers that truly do care for you, that you become friends with and that you genuinely like.&lt;/p&gt;

&lt;p&gt;Why do I find them so important? Because if you have a genuine bond with them, and they have a genuine bond with you, your partnership, your teamwork, will just be better. When there are common trust and understanding team-wide, magical things happen. There's no need for micromanagement. Work just kind of starts happening, because people are actually eager to do it, eager to come to work.&lt;/p&gt;

&lt;p&gt;Heck, even if the things you are working on are utter garbage, well you all at least got yourselves, right? You know that your manager has your back and your team's back. You know that it's not all about the KPIs and your performance. Which ironically makes you perform better.&lt;/p&gt;

&lt;p&gt;It's just a simple, yet very profound and meaningful, act of seeing you as you are - a human, not a cog in a machine. It sounds so simple, yet sometimes it takes years to reach this level and it's hard. It is. Humans are complicated creatures.&lt;/p&gt;

&lt;p&gt;How to spot level threes? They are not afraid of their failures, they can be truly vulnerable and show their weak side, to share, to make you open up a bit and relate. They have a very good relationship with their team and they know the people who work under them. That's the wrong word. They don't have people who work under them. They have people who work with them. That's one of the big differences here. Being a manager and having a partnership with the team to achieve a common goal versus having a team of slaves that you manage.&lt;/p&gt;

&lt;p&gt;I don't know what else to say here. These just are the truly empathetic people, who by not focusing on improving the performance have improved it just with that act alone. A bit ironic.&lt;/p&gt;

&lt;p&gt;How well do you know your teammates? When was the last time you've done something nice to people you work with? Are you all feeling secure in the workplace/team? These levels apply not only to managers or leaders. They can apply almost to everyone and everyone in the end influences which level of a manager you get.&lt;/p&gt;

&lt;p&gt;A manager manages the team/project, but the team also teaches the manager a lot of things. It's a two-way relationship. You are a wave - influencing other waves around you, so... Think about it for some time.&lt;/p&gt;

&lt;p&gt;Now, pause for a minute and ask yourself this: which one are you?&lt;/p&gt;

&lt;h2&gt;
  
  
  To end it off
&lt;/h2&gt;

&lt;p&gt;To sum it up. Stop focusing so much on the KPIs, Excel Sheets, Performance data. Focus on the people you work with, see them for what they are - people not resources or numbers in your excel sheets. Get to know them, their strengths and weaknesses.&lt;/p&gt;

&lt;p&gt;A good general knows where to assign a particular soldier based on his character, a good carpenter knows where to use a given piece of wood based on its characteristic. A good manager is the same. This is to the advantage of everyone. Be a good manager. Be empathic. Focus on people. That's it.&lt;/p&gt;

&lt;p&gt;We've all been there. If you currently find yourself at level one, or level two, don't worry. It's a part of the process. I'm not a full three either. Heck, I'm just a new Lead Developer, so what do I know!&lt;/p&gt;

&lt;p&gt;Check out my other articles:&lt;br&gt;
&lt;a href="https://dev.to/grski/tenancy-pattern-in-a-saas-product-2ea0"&gt;Tenancy pattern in a SaaS product&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/grski/how-i-created-my-own-blogging-system-in-less-than-100-lines-of-code-305b"&gt;How I created my own blogging system in less than 100 lines of code&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>management</category>
      <category>leadership</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
