
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
 <channel>
   <title>Tiny struggles</title>
   <link>https://tinystruggles.com/</link>
   <description>Recent content on Tiny struggles</description>
   <generator>Hugo -- gohugo.io</generator>
   <language>en-us</language>
   <lastBuildDate>Wed, 22 Jul 2122 00:00:00 +0000</lastBuildDate>
   
       <atom:link href="https://tinystruggles.com/index.xml" rel="self" type="application/rss+xml" />
   
   
     <item>
       <title>Implementing SARM on your VLA dataset in practice</title>
       <link>https://tinystruggles.com/posts/sarm_in_practice/</link>
       <pubDate>Sun, 14 Dec 2025 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/sarm_in_practice/</guid>
       <description>&lt;h2 id=&#34;1-motivation-use-big-video-dataset-optimally-for-vla-training&#34;&gt;1. Motivation: use big video dataset optimally for VLA training&lt;/h2&gt;&lt;p&gt;In the &lt;a href=&#34;https://tinystruggles.com/posts/improving_vla_training_with_reward_model/&#34;&gt;first part&lt;/a&gt; of this series, I explained what is &lt;a href=&#34;https://qianzhong-chen.github.io/sarm.github.io/&#34;&gt;SARM: Stage-Aware Reward Modeling for Long Horizon Robot Manipulation&lt;/a&gt; and how it can be used with a challenge such as &lt;a href=&#34;https://behavior.stanford.edu/challenge/index.html&#34;&gt;Stanford Behavior Challenge&lt;/a&gt; (1200h of demonstrations over 50 diverse long horizon tasks).&lt;/p&gt;&lt;p&gt;To sum up: Our main model for the robot (VLA) is trained on short windows (chunks) of data for which it predicts actions. Our SARM model is used to estimate progress within an episode and evaluates windows. We want to prioritize training on trajectory segments where the robot made meaningful progress toward task completion.&lt;/p&gt;&lt;p&gt;In this post I will explain how I actually implemented this in practice. The code is now open on github. The core of the implementation follows closely the original paper.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;We will cover the following key areas:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The design of the model &lt;strong&gt;Sequential Multimodal Architecture&lt;/strong&gt; that utilizes a Global Anchor Frame.&lt;/li&gt;&lt;li&gt;Data input shape and preparation&lt;/li&gt;&lt;li&gt;Using the model for scoring the episodes&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Visual Validation&lt;/strong&gt; of the predicted progress against our Stage-Aware Ground Truth.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;2-the-sarm-model-implementation&#34;&gt;2. The SARM Model Implementation&lt;/h2&gt;&lt;p&gt;See the &lt;a href=&#34;https://github.com/ilonajulczuk/sarm_behavior/blob/main/model.py&#34;&gt;source here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The model tackles a dual prediction problem: determining which stage of a task is being performed (classification) and how much progress has been made within that stage (regression).&lt;/p&gt;&lt;p&gt;SARM provides a principled approach to stage-aware reward modeling by:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Leveraging pretrained vision models (CLIP) for robust visual understanding&lt;/li&gt;&lt;li&gt;Fusing multimodal information through Transformers&lt;/li&gt;&lt;li&gt;Making hierarchical predictions (stage → progress within stage)&lt;/li&gt;&lt;li&gt;Handling variable-length sequences efficiently&lt;/li&gt;&lt;/ol&gt;&lt;h3 id=&#34;architecture-overview&#34;&gt;Architecture Overview&lt;/h3&gt;&lt;p&gt;This architecture is particularly well-suited for tasks that have clear sequential structure and require fine-grained progress estimation within each stage.&lt;/p&gt;&lt;p&gt;The SARM model follows a three-part design:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Encoders&lt;/strong&gt; - Process multimodal inputs (visual and proprioceptive)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Shared Backbone&lt;/strong&gt; - A Transformer that fuses information across time and modalities&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Dual Heads&lt;/strong&gt; - Separate outputs for stage classification and progress regression&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Where the symbols are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;B - batch size&lt;/li&gt;&lt;li&gt;N - sequence size (multiple frames of images/data - more on that later)&lt;/li&gt;&lt;/ul&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;┌────────────────────────────────────────────────────────────────────┐│                         INPUT LAYER                                ││                                                                    ││  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              ││  │ Image Frames │  │ Joint States │  │  Task Index  │              ││  │  (B,N,3,     │  │  (B,N,256)   │  │     (B,)     │              ││  │   224,224)   │  │              │  │              │              ││  └──────┬───────┘  └───────┬──────┘  └────────┬─────┘              │└─────────┼─────────────────-┼──────────────────┼────────────────────┘          │                  │                  │          ▼                  ▼                  ▼┌─────────────────────────────────────────────────────────────────────┐│                      ENCODER LAYER                                  ││                                                                     ││  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐               ││  │  CLIP (ViT)  │  │  LayerNorm   │  │  Embedding   │               ││  │   [Frozen]   │  │      +       │  │    Layer     │               ││  │      ↓       │  │   Linear     │  │              │               ││  │   Linear     │  │              │  │              │               ││  │  Projection  │  │  Projection  │  │              │               ││  │              │  │              │  │              │               ││  │ (512→768)    │  │ (256→768)    │  │ (50→768)     │               ││  └──────┬───────┘  └──────┬───────┘  └───────┬──────┘               ││         │                 │                  │                      ││         │   Visual        │   State          │   Task               ││         │   Embeddings    │   Embeddings     │   Embedding          ││         │   (B,N,768)     │   (B,N,768)      │   (B,1,768)          ││         └─────────┬───────┴──────────────────┘                      │└───────────────────┼─────────────────────────────────────────────────┘                    │                    ▼          ┌─────────────────┐          │  Element-wise   │          │      Sum        │          │                 │          │  Visual + State │          │    + Task       │          └────────┬────────┘                   │                   ▼          ┌─────────────────┐          │  Add Positional │          │  Bias to Frame 0│          └────────┬────────┘                   │                   │  Combined Embeddings                   │  (B,N,768)                   ▼┌────────────────────────────────────────────────────────────────────┐│                   TRANSFORMER BACKBONE                             ││                                                                    ││  ┌───────────────────────────────────────────────────────────────┐ ││  │  Transformer Encoder (8 layers)                               │ ││  │                                                               │ ││  │  ┌─────────────────────────────────────────────────────────┐  │ ││  │  │  Multi-Head Self-Attention (12 heads)                   │  │ ││  │  │  d_model = 768,                                         │  │ ││  │  │  Dropout = 0.1                                          │  │ ││  │  └─────────────────────────────────────────────────────────┘  │ ││  │                           ×8                                  │ ││  └───────────────────────────────────────────────────────────────┘ ││                                                                    ││                   (with padding mask support)                      │└──────────────────────────────┬─────────────────────────────────────┘                               │                               │  Aggregated Features                               │  (B,N,768)                               ▼┌─────────────────────────────────────────────────────────────────────┐│                         OUTPUT HEADS                                ││                                                                     ││         ┌──────────────────────┴──────────────-────────┐            ││         │                                              │            ││         ▼                                              ▼            ││  ┌─────────────────┐                          ┌─────────────────┐   ││  │  Stage Head     │                          │  Subtask Head   │   ││  │  (Classifier)   │                          │  (Regressor)    │   ││  │                 │                          │                 │   ││  │  Linear(768→512)│         ┌────────────────┤  Concat:        │   ││  │      ReLU       │         │                │  - Features(768)│   ││  │   Dropout(0.1)  │         │                │  - Logits(100)  │   ││  │  Linear(512→100)│─────────┘                │                 │   ││  │                 │                          │ Linear(868→512) │   ││  │  Stage Logits   │                          │      ReLU       │   ││  │  (B,N,100)      │                          │   Dropout(0.1)  │   ││  └─────────────────┘                          │   Linear(512→1) │   ││                                               │     Sigmoid     │   ││                                               │                 │   ││                                               │ Scalar Progress │   ││                                               │    (B,N)        │   ││                                               └─────────────────┘   │└─────────────────────────────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;data-flow-explained&#34;&gt;Data Flow Explained&lt;/h3&gt;&lt;h4 id=&#34;1-input-processing&#34;&gt;1. Input Processing&lt;/h4&gt;&lt;p&gt;The model accepts three types of inputs for each sequence:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Image Frames&lt;/strong&gt; &lt;code&gt;(B, N, 3, 224, 224)&lt;/code&gt;: A batch of N RGB images per sequence&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Joint States&lt;/strong&gt; &lt;code&gt;(B, N, D_state)&lt;/code&gt;: Robot proprioceptive information (joint angles, positions, etc.)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Task Index&lt;/strong&gt; &lt;code&gt;(B,)&lt;/code&gt;: An integer identifying which task is being performed&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Where &lt;code&gt;B&lt;/code&gt; is the batch size and &lt;code&gt;N&lt;/code&gt; is the maximum sequence length, the actual data can be shorter and then we pad it.&lt;/p&gt;&lt;p&gt;For every prediction, our model processes a sequence of frames, deliberately structured to provide maximum temporal context:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Global Anchor Frame:&lt;/strong&gt; The first frame of the episode is included in every sequence. This is a crucial engineering choice for long-horizon tasks, as it gives the Transformer a global, unchanging reference point for the task&amp;rsquo;s initial state.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Subsampled Context Frames:&lt;/strong&gt; Several preceding frames are included to capture recent history.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Current Frame:&lt;/strong&gt; The frame for which the progress prediction is required.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&#34;2-encoding-stage&#34;&gt;2. Encoding Stage&lt;/h4&gt;&lt;p&gt;Each modality is processed through its own encoder:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Visual Encoding:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Images are flattened from &lt;code&gt;(B, N, 3, 224, 224)&lt;/code&gt; to &lt;code&gt;(B*N, 3, 224, 224)&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Passed through a frozen CLIP ViT-B/32 model to extract visual features&lt;/li&gt;&lt;li&gt;CLIP outputs 512-dimensional features per image&lt;/li&gt;&lt;li&gt;Features are projected to the model dimension (768) via a linear layer&lt;/li&gt;&lt;li&gt;Reshaped back to &lt;code&gt;(B, N, 768)&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;State Encoding:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Joint states are normalized using LayerNorm&lt;/li&gt;&lt;li&gt;Projected from dimension 256 to 768 via a linear layer&lt;/li&gt;&lt;li&gt;Output: &lt;code&gt;(B, N, 768)&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Task Encoding:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Task index is converted to a learned embedding vector&lt;/li&gt;&lt;li&gt;The embedding is replicated across the sequence: &lt;code&gt;(B,) → (B, 1, 768)&lt;/code&gt;&lt;/li&gt;&lt;li&gt;This embedding is broadcast and added to all timesteps&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&#34;3-multimodal-fusion&#34;&gt;3. Multimodal Fusion&lt;/h4&gt;&lt;p&gt;The three encoded representations are combined:&lt;/p&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;input_embeddings = visual_embeddings + state_embeddings + task_embedding&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Additionally, a learned positional bias is added only to the first frame:&lt;/p&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;input_embeddings[:, 0, :] += positional_bias&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This creates a unified representation &lt;code&gt;(B, N, 768)&lt;/code&gt; that contains information from all modalities.&lt;/p&gt;&lt;h4 id=&#34;4-transformer-backbone&#34;&gt;4. Transformer Backbone&lt;/h4&gt;&lt;p&gt;The combined embeddings are processed through an 8-layer Transformer encoder:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Architecture&lt;/strong&gt;: Standard Transformer encoder with 12 attention heads&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Dimensions&lt;/strong&gt;: 768-dimensional hidden states, 3072-dimensional feedforward layers&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Padding Support&lt;/strong&gt;: The model accepts an optional padding mask &lt;code&gt;(B, N)&lt;/code&gt; where &lt;code&gt;True&lt;/code&gt; indicates padded positions&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Output&lt;/strong&gt;: Aggregated features &lt;code&gt;(B, N, 768)&lt;/code&gt; that capture temporal and multimodal dependencies&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&#34;5-dual-output-heads&#34;&gt;5. Dual Output Heads&lt;/h4&gt;&lt;p&gt;The model produces two types of predictions:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Stage Head (Classification):&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Takes the aggregated features &lt;code&gt;(B, N, 768)&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Passes through: Linear(768→512) → ReLU → Dropout → Linear(512→100)&lt;/li&gt;&lt;li&gt;Outputs stage logits &lt;code&gt;(B, N, 100)&lt;/code&gt; representing 100 possible task stages:&lt;ul&gt;&lt;li&gt;100 is a maximum number of task stages supported, in practice the stages will be task dependent&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;Trained with cross-entropy loss&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Subtask Head (Regression):&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Concatenates aggregated features with stage logits: &lt;code&gt;[features, stage_logits]&lt;/code&gt; → &lt;code&gt;(B, N, 868)&lt;/code&gt;&lt;/li&gt;&lt;li&gt;This conditioning allows progress estimation to be stage-aware&lt;/li&gt;&lt;li&gt;Passes through: Linear(868→512) → ReLU → Dropout → Linear(512→1) → Sigmoid&lt;/li&gt;&lt;li&gt;Outputs scalar progress &lt;code&gt;(B, N)&lt;/code&gt; in the range [0, 1]&lt;/li&gt;&lt;li&gt;Trained with MSE loss&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;loss-computation&#34;&gt;Loss Computation&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;SARMWithLoss&lt;/code&gt; wrapper handles training:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Masking&lt;/strong&gt;: Only non-padded positions are included in loss calculation&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Stage Loss&lt;/strong&gt;: Cross-entropy between predicted logits and ground truth stage labels&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Progress Loss&lt;/strong&gt;: MSE between predicted progress and ground truth progress values&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Total Loss&lt;/strong&gt;: Weighted sum of both losses (default weights: 1.0 each)&lt;/li&gt;&lt;/ol&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;total_loss &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (stage_loss_weight &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;×&lt;/span&gt; stage_loss) &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; (progress_loss_weight &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;×&lt;/span&gt; progress_loss)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Including the loss calculation within the model wrapper made the training code simpler and more standard.&lt;/p&gt;&lt;h3 id=&#34;key-design-decisions&#34;&gt;Key Design Decisions&lt;/h3&gt;&lt;p&gt;These decisions follow the original SARM paper:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why freeze CLIP?&lt;/strong&gt;CLIP is pretrained on massive image-text datasets and provides robust visual features. Freezing it prevents overfitting on smaller robotics datasets and reduces computational cost.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why condition subtask head on stage predictions?&lt;/strong&gt;We estimate progress within a stage, not the whole episode, so it&amp;rsquo;s stage dependent.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why add positional bias only to the first frame?&lt;/strong&gt;The first frame often contains important context about the initial state. The positional bias helps the model distinguish the starting point from subsequent frames. Supposedly such anchoring is very effective for video models.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why use variable-length sequences with padding?&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;We use Rewind Augmentation when we sometimes generate longer sequences that &amp;lsquo;mess up&amp;rsquo; progress on purpose by replaying older frames in the reverse order. Because of that we need to handle sequences of varied length. This augmentation is critical for the model to learn how undoing progress looks like.&lt;/p&gt;&lt;h2 id=&#34;3-complex-data-preparation&#34;&gt;3. Complex data preparation&lt;/h2&gt;&lt;p&gt;The data for SARM has to be prepared in a very particular way.&lt;/p&gt;&lt;p&gt;The core of the sampling is implemented in the custom dataloaders &lt;a href=&#34;https://github.com/ilonajulczuk/sarm_behavior/blob/main/dataloader.py&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&#34;temporal-sampling-strategy&#34;&gt;Temporal Sampling Strategy&lt;/h3&gt;&lt;p&gt;SARM doesn&amp;rsquo;t sample frames uniformly. Instead, it uses a sophisticated sampling strategy designed to provide temporal context:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;prepare_indices&lt;/span&gt;(ep_first_frame_idx, idx, skip_count&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                   default_length&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;8&lt;/span&gt;, rewind_prob&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0.05&lt;/span&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# Sample backwards in time with skip_count intervals&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    indices &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [idx &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; skip_count &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; range(default_length)]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# Always include the first frame of the episode&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    indices&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append(ep_first_frame_idx)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# Reverse so time flows forward&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    indices &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; list(reversed(indices))&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 5% chance: add &amp;#34;rewound&amp;#34; frames for temporal augmentation&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; random() &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; rewind_prob:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        num_extra &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; random&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;integers(&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        indices &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; [indices[&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; i] &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; range(&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, num_extra &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;)]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; indices&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This creates sequences with the following structure:&lt;/p&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Sequence construction (skip_count=30, ~1 second at 30 FPS):┌───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐│Frame 0│ t-7s  │ t-6s  │ t-5s  │ t-4s  │ t-3s  │ t-2s  │ t-1s  │  t    ││(start)│       │       │       │       │       │       │       │(curr) │└───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘With 5% probability, add rewind frames:┌───────┬───────┬─────────────┬───────┬───────┬───────┐│...    │  t    │ t-1s (again)│ t-2s  │ t-3s  │(curr) │└───────┴───────┴─────────────┴───────┴───────┴───────┘&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Why this design?&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Always anchor to episode start&lt;/strong&gt;: Frame 0 provides consistent context about initial conditions&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Uniform temporal spacing&lt;/strong&gt;: 1-second intervals capture motion patterns without redundancy&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Rewind augmentation&lt;/strong&gt;: Teaches the model temporal reversibility and robustness&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Future context avoided&lt;/strong&gt;: Model only sees past and present, not future frames&lt;/li&gt;&lt;/ol&gt;&lt;h3 id=&#34;delta-timestamps-pattern&#34;&gt;Delta Timestamps Pattern&lt;/h3&gt;&lt;p&gt;The sampling strategy is complemented by a clever timestamping scheme:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;DELTA_TIMESTAMPS &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [HIGH_NEGATIVE_TIMEDELTA] &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; [&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;7&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; i &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; range(&lt;span style=&#34;color:#ae81ff&#34;&gt;8&lt;/span&gt;)]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Results in: [1e6, -7, -6, -5, -4, -3, -2, -1, 0]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When applied to current timestamp &lt;code&gt;t&lt;/code&gt;:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Frame 0: Gets timestamp ≈ -∞ (approximated as episode start)&lt;/li&gt;&lt;li&gt;Frames 1-7: Get timestamps [t-7, t-6, &amp;hellip;, t-1]&lt;/li&gt;&lt;li&gt;Frame 8: Gets timestamp t (current)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This ensures consistent temporal windows regardless of where you are in the episode.My custom dataset &lt;code&gt;SARMDataset&lt;/code&gt; uses a dataset provided by the BEHAVIOR codebase under the hood that allows specifying delta timestamps for more efficient sampling.&lt;/p&gt;&lt;h3 id=&#34;variable-length-sequence-handling&#34;&gt;Variable-Length Sequence Handling&lt;/h3&gt;&lt;p&gt;Real episodes have variable lengths, and sequences can have different numbers of frames. SARM handles this with:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;collate_fn&lt;/span&gt;(batch):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# Find max sequence length in batch&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    max_length &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; max(sample[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sequence_length&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; sample &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; batch)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# Pad all sequences to max_length&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    batched_images &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; torch&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;zeros(batch_size, max_length, C, H, W)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    batched_padding_mask &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; torch&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ones(batch_size, max_length, dtype&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;torch&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;bool)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i, sample &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; enumerate(batch):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        seq_len &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; sample[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sequence_length&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        batched_images[i, :seq_len] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; sample[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;images&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        batched_padding_mask[i, :seq_len] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;False&lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# False = valid, True = padding&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The padding mask is then passed to the Transformer to ensure padded positions don&amp;rsquo;t contribute to attention or loss:&lt;/p&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Example batch with lengths [9, 11, 13, 9]:Padded to max_length=13:┌─────────────┬─────────────┬─────────────┬─────────────┐│ Seq 1 (9)   │ Seq 2 (11)  │ Seq 3 (13)  │ Seq 4 (9)   │├─────────────┼─────────────┼─────────────┼─────────────┤│ [V][V]...[V]│ [V][V]...[V]│ [V][V]...[V]│ [V][V]...[V]││ [P][P][P][P]│ [P][P]      │             │ [P][P][P][P]│└─────────────┴─────────────┴─────────────┴─────────────┘  V = Valid token  P = Padding token (masked out)&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;inference-sampling-strategy&#34;&gt;Inference Sampling Strategy&lt;/h3&gt;&lt;p&gt;To use SARM for VLA training, we need to run our original video dataset through SARM.&lt;/p&gt;&lt;p&gt;But that dataset was huge to begin with! But we don&amp;rsquo;t need to evaluate every frame (with its proceeding sequence).&lt;/p&gt;&lt;p&gt;With 5-second sampling at 30 FPS, I evaluate only 1 out of every 150 frames (5s × 30 FPS), reducing computational cost by 150×.&lt;/p&gt;&lt;p&gt;I implemented a following dataloader (&lt;a href=&#34;https://github.com/ilonajulczuk/sarm_behavior/blob/main/dataloader.py#L933&#34;&gt;source&lt;/a&gt;).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Why jitter?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Jitter prevents the model from overfitting to fixed timestamps and produces more robust progress estimates by sampling at slightly varied intervals rather than exact multiples of 5 seconds.&lt;/p&gt;&lt;p&gt;See &lt;a href=&#34;https://github.com/ilonajulczuk/sarm_behavior/blob/main/INFERENCE_README.md&#34;&gt;INFERENCE_README&lt;/a&gt; for more details on the inference.&lt;/p&gt;&lt;h3 id=&#34;translating-the-progress-to-vla-training-weights&#34;&gt;Translating the progress to VLA training weights&lt;/h3&gt;&lt;p&gt;Additionally I implement the progress mapping to the weights following the SARM paper (equations 8-9):- Computes progress deltas r̂ᵢ = φ(t+Δ) - φ(t)- Uses running statistics (μ, σ) to normalize- Applies linear ramp between (μ - 2σ) and (μ + 2σ)- Optionally uses threshold κ for decisive weighting&lt;/p&gt;&lt;p&gt;See the code in &lt;a href=&#34;https://github.com/ilonajulczuk/sarm_behavior/blob/main/weight_utils.py&#34;&gt;weight utils&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&#34;4-training--results&#34;&gt;4. Training &amp;amp; Results&lt;/h2&gt;&lt;p&gt;This implementation of SARM was multi-task, however since there was so much data, I decided that it would be easier to evaluate and visualize it on a single task first.&lt;/p&gt;&lt;p&gt;Having 200 episodes for each task, I divided the data into the following sets:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&amp;ldquo;train_episodes&amp;rdquo;: 1-90,&lt;/li&gt;&lt;li&gt;&amp;ldquo;val_episodes&amp;rdquo;: 91-105,&lt;/li&gt;&lt;li&gt;&amp;ldquo;test_episodes&amp;rdquo;: 106-200&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In general performance on the validation set wasn&amp;rsquo;t the best indicator of actual model performance when I analyzed it on the test dataset. Training for more steps was helpful.&lt;/p&gt;&lt;p&gt;For training details see the &lt;a href=&#34;https://github.com/ilonajulczuk/sarm_behavior/blob/main/config.json&#34;&gt;config&lt;/a&gt; and the &lt;a href=&#34;https://github.com/ilonajulczuk/sarm_behavior/blob/main/train_simple.py&#34;&gt;training script&lt;/a&gt;. The 10k-step snapshot has been trained on a single RTX5090 over several hours. The model is also compatible with training on MPS. The key bottleneck was the dataset access and video processing.&lt;/p&gt;&lt;h3 id=&#34;visualizations--analysis&#34;&gt;Visualizations &amp;amp; analysis&lt;/h3&gt;&lt;p&gt;I performed detailed analysis on how well the models were predicting the progress &lt;a href=&#34;https://github.com/ilonajulczuk/sarm_behavior/blob/main/ground_truth_vis.ipynb&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;h4 id=&#34;ground-truth&#34;&gt;Ground Truth&lt;/h4&gt;&lt;p&gt;First, it&amp;rsquo;s important how the &amp;lsquo;ground truth&amp;rsquo; data looks like, here is a visualization:&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/sarm_episode_pg_hu_468f0b29a666143d.png&#34;&gt;&lt;p&gt;Based on the data annotations and the stage statistics I was able to generate our &amp;lsquo;Ground truth&amp;rsquo; of progress. It was also a useful sanity check if the ground truth data looks right, e.g. having negative progress in the ground truth data would mean that there were bugs. We were only adding &amp;rsquo;negative progress&amp;rsquo; through the Rewind augmentation later on.&lt;/p&gt;&lt;h4 id=&#34;comparing-models-vs-ground-truth-and-each-other&#34;&gt;Comparing models vs ground truth and each other&lt;/h4&gt;&lt;p&gt;Model checkpoint comparison vs &amp;lsquo;ground truth&amp;rsquo; on a sample of episodes:&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/visualization_checkpoint_comparison_hu_d94bc6bb467032ea.png&#34;&gt;&lt;p&gt;The first 3 episodes were in the training data and the 2 last ones weren&amp;rsquo;t present.You can see here that the yellow (10k steps) model is better fitted to the data in the training set.&lt;/p&gt;&lt;h4 id=&#34;understanding-the-bias-of-the-model&#34;&gt;Understanding the bias of the model&lt;/h4&gt;&lt;p&gt;So the model wasn&amp;rsquo;t perfect, what type of mistakes was it making?&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/model_analysis_hu_a5e5f8ab79f86487.png&#34;&gt;&lt;p&gt;Overall, the model was leaning towards underestimating the progress. And the key problem was from predicting wrong stage number.&lt;/p&gt;&lt;h3 id=&#34;applicability-and-limitations&#34;&gt;Applicability and Limitations&lt;/h3&gt;&lt;p&gt;The caveat here that the task 8 was multimodal, the stages could be done in variable order, breaking the fundamental assumption of the fix stage order in SARM. Visualizations for a task fitting SARM assumptions would look better.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;The SARM Assumption:&lt;/strong&gt; Stage-Aware modeling assumes a generally &lt;strong&gt;linear path&lt;/strong&gt; through semantic checkpoints (Stage 1 $\rightarrow$ Stage 2 $\rightarrow$ Stage 3&amp;hellip;).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;The Failure Case: Multimodal Progress:&lt;/strong&gt; If a task allows for subtasks to be completed in an &lt;em&gt;arbitrary order&lt;/em&gt; (e.g., &amp;ldquo;Tidy up the room&amp;rdquo;), the progress estimation becomes inherently &lt;strong&gt;multimodal&lt;/strong&gt;, and our regression model, forced to average these possibilities, loses accuracy.&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;handling-different-sequences-of-stages-in-demonstrations&#34;&gt;Handling different sequences of stages in demonstrations&lt;/h3&gt;&lt;h4 id=&#34;removing-outliers&#34;&gt;Removing outliers&lt;/h4&gt;&lt;p&gt;If the majority of demonstrations are done in a consistent way, then we can remove the outlier demonstrations that create confusion. (Annotations to generate ground truth are enough to blacklist such episodes).&lt;/p&gt;&lt;h4 id=&#34;subtask-splitting&#34;&gt;Subtask splitting&lt;/h4&gt;&lt;p&gt;The SARM model implemented here can handle multiple tasks.&lt;/p&gt;&lt;p&gt;Therefore, if a task can be done using different sequences of stages, we can transform it into set of related tasks with different demonstration variations.&lt;/p&gt;&lt;p&gt;If there are multiple different ways represented in similar proportions, e.g. &amp;lsquo;pick up toy 1&amp;rsquo; then &amp;lsquo;pick up toy 2&amp;rsquo;, and the reverse, we can change into two tasks pick_up_toys_1_2, pick_up_toys_2_1.&lt;/p&gt;&lt;h4 id=&#34;equal-sampling&#34;&gt;Equal sampling&lt;/h4&gt;&lt;p&gt;We can also decide not use SARM for such tasks.&lt;/p&gt;&lt;h3 id=&#34;sarm-in-behavior-challenge&#34;&gt;SARM in Behavior challenge&lt;/h3&gt;&lt;p&gt;For the BEHAVIOR challenge specifically, we were very time constrained, and we ended up not having enough time to apply the model for the final checkpoint training, additionally, only about 30% of tasks fulfilled the fixed stage ordering for SARM.&lt;/p&gt;&lt;p&gt;We performed quick fine tuning with weighted sampling earlier on a subset of data (not based on SARM), but it was difficult to see if it was actually helpful (eval in general was pretty challenging).&lt;/p&gt;&lt;p&gt;Despite these challenges, SARM remains a promising approach for datasets with proper stage annotations and sequential task structure. Our analysis on the 30% of tasks with fixed stage ordering showed the model could accurately track progress. For the remaining tasks, the subtask splitting approach outlined above could make SARM applicable, potentially enabling more efficient VLA training through intelligent data selection across the full dataset.&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Improve Your VLA training: Using Reward Models to Filter Best Training Data</title>
       <link>https://tinystruggles.com/posts/improving_vla_training_with_reward_model/</link>
       <pubDate>Wed, 29 Oct 2025 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/improving_vla_training_with_reward_model/</guid>
       <description>&lt;p&gt;In recent years, there has been huge progress in AI for robotic manipulation. VLAs such as $\pi_0$ ($\pi_{0.5}$) are the current state of the art, but it&amp;rsquo;s still difficult to get good performance on complex, long-horizon tasks.&lt;/p&gt;&lt;p&gt;Exactly the type of tasks as in the &lt;a href=&#34;https://behavior.stanford.edu/challenge/index.html&#34;&gt;Stanford BEHAVIOR Challenge&lt;/a&gt; that I have been working on (as a group effort) for the last couple of weeks. The challenge consists of 50 full-length household tasks, with 200 demonstrations each. And the tasks are long, 1200h, often a single demonstration is longer than 10 minutes.&lt;/p&gt;&lt;p&gt;For imitation learning - having a great demonstration dataset is a key differentiator. How to improve the dataset? If it&amp;rsquo;s small, you can always record more demos, even though it&amp;rsquo;s expensive and time-consuming. But what if it&amp;rsquo;s already big?  In BEHAVIOR challenge, the dataset is already huge, in original quality it&amp;rsquo;s about 2TB and training on its entirety could be taking weeks.&lt;/p&gt;&lt;p&gt;It&amp;rsquo;s hard to assess the quality of the demonstrations as there are 12000h of it. Maybe there&amp;rsquo;s a better way to train than just doing behavior cloning on the entirety of the dataset?&lt;/p&gt;&lt;p&gt;One of the approaches we wanted to try as a group is using some RL methods to augment the VLA training. This is what &lt;a href=&#34;https://qianzhong-chen.github.io/sarm.github.io/&#34;&gt;&lt;strong&gt;SARM: Stage-Aware Reward Modeling for Long Horizon Robot Manipulation&lt;/strong&gt;&lt;/a&gt; addresses. The paper uses the challenging task of T-shirt folding (long horizon task somewhat equivalent to the BEHAVIOR tasks) to demonstrate why standard fine-tuning fails and presents a clever, two-part framework to solve it:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;train a reward model that assigns high rewards to samples with high progress (using two staged approach)&lt;/li&gt;&lt;li&gt;use the reward models in the fine tuning of a target model (incorporating it in the loss function), it makes the model to pay attention more to demonstrations with high progress&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This post is a deep dive into what makes this paper work, focusing on the tricky parts:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Quality issues of large datasets&lt;/strong&gt; fine-tuning on a 200-hour dataset can result in 0% success.&lt;/li&gt;&lt;li&gt;How SARM re-thinks &lt;strong&gt;&amp;ldquo;progress&amp;rdquo; labeling&lt;/strong&gt; to create a stable reward signal&lt;/li&gt;&lt;li&gt;The &amp;ldquo;Rewind&amp;rdquo; data augmentation trick that&amp;rsquo;s &lt;strong&gt;essential for learning &amp;ldquo;failure&amp;rdquo;&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Why this reward scheme is valuable: The paper&amp;rsquo;s two-part evaluation, and why one test is for &lt;strong&gt;accuracy&lt;/strong&gt; and the other is for &lt;strong&gt;usefulness&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;How this method differs from the &lt;strong&gt;typical use of reward models in RL&lt;/strong&gt;.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;as well as translating the ideas into practical implementation on a large dataset.&lt;/p&gt;&lt;h3 id=&#34;1-the-problem-garbage-data-kills-pi_0-fine-tuning&#34;&gt;1. The Problem: &amp;ldquo;Garbage Data&amp;rdquo; Kills $\pi_0$ Fine-Tuning&lt;/h3&gt;&lt;p&gt;The &lt;a href=&#34;https://qianzhong-chen.github.io/sarm.github.io/&#34;&gt;SARM Paper&amp;rsquo;s&lt;/a&gt; goal is to fine-tune $\pi_0$ on T-shirt folding, a &amp;ldquo;long-horizon, contact-rich manipulation task&amp;rdquo; involving deformable objects. The authors break down the task&amp;rsquo;s complexity:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Easy:&lt;/strong&gt; &amp;ldquo;Picking the shirt from a box&amp;rdquo; (~5 seconds).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Medium:&lt;/strong&gt; &amp;ldquo;Folding a T-shirt &lt;em&gt;from a flattened state&lt;/em&gt;&amp;rdquo; (30-60 seconds).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Hard:&lt;/strong&gt; &amp;ldquo;Folding from a &lt;em&gt;crumpled state&lt;/em&gt;&amp;rdquo; (1-3 minutes).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The &amp;ldquo;Hard&amp;rdquo; task is the real challenge, as it requires long-term planning and handling uncertainty. A look at the paper&amp;rsquo;s &amp;ldquo;dense annotations&amp;rdquo; shows the &amp;ldquo;fold&amp;rdquo; stage alone consists of 5-7 distinct sub-tasks, like &amp;ldquo;grab near side and fold,&amp;rdquo; &amp;ldquo;rotate the tshirt 90 deg,&amp;rdquo; etc.&lt;/p&gt;&lt;p&gt;The standard approach is to fine-tune $\pi_0$ on a large dataset using standard Behavior Cloning (BC). The authors did this with a 200-hour dataset, &amp;ldquo;BC-All,&amp;rdquo; and the results were stark:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Easy Task:&lt;/strong&gt; 100% success.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Medium Task:&lt;/strong&gt; 8% success.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Hard Task:&lt;/strong&gt; &lt;strong&gt;0% success&lt;/strong&gt; .&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The problem isn&amp;rsquo;t $\pi_0$. It&amp;rsquo;s that a 200-hour dataset of a 3-minute task is inevitably &amp;ldquo;noisy.&amp;rdquo; It is filled with &amp;ldquo;suboptimal trajectories&amp;rdquo;—pauses, fumbled grasps, and inefficient recovery motions. Standard BC learns from this &amp;ldquo;garbage data&amp;rdquo; just as much as it learns from expert motions, leading to a confused policy that imitates the failures.&lt;/p&gt;&lt;h3 id=&#34;2-the-solution-weighted-fine-tuning-repurposing-the-reward-model&#34;&gt;2. The Solution: Weighted Fine-Tuning (Repurposing the reward model)&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Reward-Aligned Behavior Cloning (RA-BC)&lt;/strong&gt; introduced in the paper tries to address this problem by learning a reward model, that is then used to weigh the loss during the VLA fine tuning (BC).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;How Reward Models are &lt;em&gt;Usually&lt;/em&gt; Used in RL&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;In RL, the agent tries to select actions that maximize rewards.Typically, a reward model is used &lt;em&gt;online&lt;/em&gt;. In a framework like the &lt;a href=&#34;https://arxiv.org/abs/2505.10911&#34;&gt;ReWiND paper&lt;/a&gt;, an agent (trained with an RL algorithm like SAC) interacts with the world, and the reward model provides a &lt;em&gt;live, dense reward signal&lt;/em&gt; to guide its learning. The agent then learns from this feedback over millions of interactions.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;SARM&amp;rsquo;s &amp;ldquo;Offline&amp;rdquo; Approach&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The SARM paper is primarily about imitation learning (RL is briefly explored in the appendix). Instead of RL, it uses its reward model &lt;em&gt;offline&lt;/em&gt; as a one-time data filter. The RA-BC (reward aligned behavior cloning) framework is a &lt;strong&gt;&amp;ldquo;weighted&amp;rdquo; fine-tuning of $\pi_0$&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Here’s how it works:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;A reward model (SARM) is trained to be an expert at scoring progress.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;This reward model scores every single clip in the entire 200-hour noisy dataset.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;A &amp;ldquo;progress delta&amp;rdquo; is calculated for each clip:  $\hat{r} = progress_{end} - progress_{start}$&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;This $\hat{r}$ is mapped to a weight $w$ for the BC loss function between 0 and 1.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;The final $\pi_0$ fine-tuning is done with this weighted loss: ${L}_{RA\cdot BC}(\theta) = \frac{\sum w_i \cdot \text{loss}(i)}{\sum w_i}$.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;This forces the $\pi_0$ model to &lt;strong&gt;only learn from the high-quality, high-progress segments&lt;/strong&gt; and completely ignore the &amp;ldquo;garbage data&amp;rdquo;. The success of this entire method now hinges on one thing: the quality of the reward model.&lt;/p&gt;&lt;h3 id=&#34;3-sarm-building-a-reward-model-that-actually-works&#34;&gt;3. SARM: Building a Reward Model That &lt;em&gt;Actually&lt;/em&gt; Works&lt;/h3&gt;&lt;p&gt;The paper&amp;rsquo;s core contribution is its two-ingredient recipe for building a robust reward model.&lt;/p&gt;&lt;h4 id=&#34;ingredient-1-stage-aware-labeling-to-learn-progress&#34;&gt;Ingredient 1: &amp;ldquo;Stage-Aware&amp;rdquo; Labeling (To Learn &lt;em&gt;Progress&lt;/em&gt;)&lt;/h4&gt;&lt;p&gt;The paper argues that to describe progress, one needs to identify a task stage and the progress within that stage for multi-stage, varied-trajectory tasks.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Prior work often relies on frame indices as labels (&amp;hellip;). While thismay suffice for short tasks with fixed duration, such as “pick up the cup,” it fails for tasks like“fold the T-shirt,” where trajectories vary greatly, task duration is not fixed, and motion sequencesdiffer across demonstrations. For example, in T-shirt folding, the flattening phase may require moreor fewer motions depending on shirt placement or fabric configuration, yet frame-based labelingonly reflects elapsed time. As a result, identical task states (e.g., a fully flattened shirt) can receiveprogress values ranging from 0.2 to 0.8, introducing severe label noise that harms reward modellearning and downstream policy training.&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;The SARM Solution:&lt;/strong&gt;&lt;ol&gt;&lt;li&gt;Define semantic &lt;strong&gt;Stages&lt;/strong&gt; (e.g., &amp;ldquo;Grab,&amp;rdquo; &amp;ldquo;Flatten,&amp;rdquo; &amp;ldquo;Fold&amp;rdquo;) .&lt;/li&gt;&lt;li&gt;Calculate the &lt;strong&gt;dataset-wide average time proportion&lt;/strong&gt; for each stage (e.g., &amp;ldquo;Flatten&amp;rdquo; takes 25% of the total time, &lt;em&gt;on average&lt;/em&gt;).&lt;/li&gt;&lt;li&gt;This creates &lt;strong&gt;fixed progress checkpoints&lt;/strong&gt;. &amp;ldquo;Grab&amp;rdquo; is always the 0.0 $\rightarrow$ 0.1 window, and &amp;ldquo;Flatten&amp;rdquo; is always 0.1 $\rightarrow$ 0.35, etc..&lt;/li&gt;&lt;li&gt;Labels are then generated by &lt;strong&gt;interpolating between these fixed checkpoints&lt;/strong&gt;.&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Now, the &amp;ldquo;fully flat shirt&amp;rdquo; state &lt;strong&gt;always&lt;/strong&gt; gets a label of 0.35 (or some other consistent value). This provides a stable, consistent signal for the model to learn.&lt;/p&gt;&lt;p&gt;Note: this stage + progress within the stage idea is also used for adding a &amp;lsquo;simple system 2&amp;rsquo; for a VLA system that helps the model not get lost at long horizon tasks. It&amp;rsquo;s especially helpful when the state isn&amp;rsquo;t fully captured and observations are not Markovian (e.g. next stage might look the same as the previous stage, but we need to do different things now).&lt;/p&gt;&lt;h4 id=&#34;ingredient-2-rewind-augmentation-to-learn-failure&#34;&gt;Ingredient 2: &amp;ldquo;Rewind Augmentation&amp;rdquo; (To Learn &lt;em&gt;Failure&lt;/em&gt;)&lt;/h4&gt;&lt;p&gt;In our imitation learning dataset, we don&amp;rsquo;t want to teach the robot how to make mistakes on purpose. We might want to teach it how to recover from problems, but not how to introduce them in the first place.The training demos are all (mostly) successful, but some accidental failures (and recoveries) might sneak in.&lt;/p&gt;&lt;p&gt;How can we avoid accidentally teaching the robot to make mistakes?&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;The Solution:&lt;/strong&gt; The paper adopts a key technique from the ReWiND paper: &lt;strong&gt;&amp;ldquo;rewind augmentation&amp;rdquo;&lt;/strong&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;How it Works:&lt;/strong&gt; During training, the system takes a successful video clip (e.g., frames 1-10) and appends frames from earlier in the clip &lt;em&gt;in reverse order&lt;/em&gt; (e.g., frames 1-10 are followed by frames 8, 7, 6).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;The Result:&lt;/strong&gt; The model is explicitly trained to predict &lt;em&gt;decreasing&lt;/em&gt; progress scores for this &amp;ldquo;rewound&amp;rdquo; section. This teaches the model to recognize and penalize actions that &lt;em&gt;undo&lt;/em&gt; progress, which is &amp;ldquo;essential for building reward models that generalize to real-world policies&amp;rdquo;.&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;4-demystifying-the-evaluation-accuracy-vs-usefulness&#34;&gt;4. Demystifying the Evaluation: &amp;ldquo;Accuracy&amp;rdquo; vs. &amp;ldquo;Usefulness&amp;rdquo;&lt;/h3&gt;&lt;p&gt;How is the SARM model assessed? The model is evaluated on its own as well as in the RA-BC setup (compared to simple fine tuning or other reward models).&lt;/p&gt;&lt;p&gt;The authors evaluate their SARM model with two different methods in Table 1, and they are &lt;em&gt;not&lt;/em&gt; the same . It&amp;rsquo;s a test of &lt;strong&gt;&amp;ldquo;Accuracy&amp;rdquo; vs. &amp;ldquo;Usefulness&amp;rdquo;&lt;/strong&gt;.&lt;/p&gt;&lt;h4 id=&#34;test-1-loss-on-human-demonstrations-model-accuracy&#34;&gt;Test 1: Loss on human demonstrations (Model Accuracy)&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Question:&lt;/strong&gt; Is our model &lt;em&gt;accurate&lt;/em&gt; at predicting the ground-truth labels on &lt;em&gt;clean, unseen&lt;/em&gt; human demos?&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Method:&lt;/strong&gt; A standard regression test. It measures the &lt;strong&gt;Mean Squared Error (MSE)&lt;/strong&gt; between the model&amp;rsquo;s predicted progress and the true stage-aware label.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Result:&lt;/strong&gt; A &lt;strong&gt;low&lt;/strong&gt; score is better. This just proves the model learned what it was told.&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&#34;test-2-rollout-of-robot-policy-model-usefulness&#34;&gt;Test 2: &amp;ldquo;Rollout of robot policy&amp;rdquo; (Model Usefulness)&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Question:&lt;/strong&gt; Are the model&amp;rsquo;s scores &lt;em&gt;useful&lt;/em&gt; for judging &lt;em&gt;messy, real-world&lt;/em&gt; robot rollouts? Can it tell success from failure?&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Method:&lt;/strong&gt; This is a &lt;em&gt;classification&lt;/em&gt; test, and it has an extra step.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;A robot policy is rolled out (interacts with the environment to perform the task) to create multiple episodes that are then labeled as Successful (SE), Partially Successful (PSE) or Failure (FE).&lt;/li&gt;&lt;li&gt;The &lt;strong&gt;same SARM model&lt;/strong&gt; (trained only on clean demos) generates its normal progress scores for a messy, out-of-distribution robot video.&lt;/li&gt;&lt;li&gt;The &lt;em&gt;evaluators&lt;/em&gt; (not the model) apply a ruleset to these scores: &lt;strong&gt;&amp;ldquo;IF&lt;/strong&gt; (final_score &amp;gt; 0.8) &lt;strong&gt;AND&lt;/strong&gt; (avg_progress_last_third &amp;gt; 0.6) &lt;strong&gt;THEN&lt;/strong&gt; classify as &amp;lsquo;Success&amp;rsquo;&amp;rdquo;.&lt;/li&gt;&lt;li&gt;The final score ($\rho$) measures how well these &lt;em&gt;predicted labels&lt;/em&gt; match the &lt;em&gt;true human labels&lt;/em&gt; (SE, PSE, FE).&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Real-life interactions with the environment are much messier than the &amp;lsquo;ideal demos&amp;rsquo;, so the data from such rollout (demonstration) is Out of Distribution (very different) from what was encountered in the demonstration. This test proves the model is robust and its scores are meaningful for filtering real-world failures (and it would also make it significantly more useful for online RL where OOD problems are deadly). The paper shows the baseline ReWiND reward model &lt;em&gt;fails&lt;/em&gt; this &amp;ldquo;usefulness&amp;rdquo; test, which is why the &lt;code&gt;RA-BC-ReWIND&lt;/code&gt; policy ultimately failed.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;5-how-to-apply-sarm-to-your-own-dataset-a-4-step-guide&#34;&gt;5. How to Apply SARM to Your Own Dataset (A 4-Step Guide)&lt;/h3&gt;&lt;p&gt;This section will explain how the ideas from the paper could be applied to a real-world example, like the &lt;strong&gt;BEHAVIOR challenge dataset&lt;/strong&gt; (50 tasks, 200 episodes/task, all annotated).&lt;/p&gt;&lt;h4 id=&#34;step-1-define-canonical-stages-per-task&#34;&gt;Step 1: Define Canonical Stages (Per-Task)&lt;/h4&gt;&lt;p&gt;This is the most critical step. For each of your 50 tasks, define its unique, semantic stages.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Task 12: (Preparing Lunch Box):&lt;/strong&gt; &amp;ldquo;Put both apple halves, the club sandwich, and the chocolate chip cookie from the chopping board on the kitchen countertop into the packing box on the countertop. Then take the bottle of tea out of the refrigerator, put it into the same box, and close the refrigerator when you’re done.&amp;rdquo;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Task 7 (Picking Up Toys):&lt;/strong&gt; &amp;ldquo;Put all the toys in the child’s room - the three board games (two on the bed and one on the table), the two jigsaw puzzles on the table, and the tennis ball on the table - inside the toy box on the table in the child’s room.&amp;rdquo;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The tasks have text descriptions as well as detailed annotations with time frames. Each annotation can correspond to a separate stage.&lt;/p&gt;&lt;p&gt;Annotation file example from the behavior dataset:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;task_name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;preparing lunch box&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;data_folder&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;meta_data&amp;#34;&lt;/span&gt;: {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;task_duration&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;7876&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;valid_duration&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ae81ff&#34;&gt;90&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ae81ff&#34;&gt;7966&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_annotation&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_idx&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_id&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_description&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;move to&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;object_id&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;packing_box_210&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;manipulating_object_id&amp;#34;&lt;/span&gt;: [],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;spatial_prefix&amp;#34;&lt;/span&gt;: [],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;frame_duration&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ae81ff&#34;&gt;90&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ae81ff&#34;&gt;343&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;mp_ef&amp;#34;&lt;/span&gt;: [],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_type&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;navigation&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        },&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_idx&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_id&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_description&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pick up from&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;object_id&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;packing_box_210&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;countertop_kelker_0&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;manipulating_object_id&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;packing_box_210&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;spatial_prefix&amp;#34;&lt;/span&gt;: [],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;frame_duration&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ae81ff&#34;&gt;344&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ae81ff&#34;&gt;619&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;mp_ef&amp;#34;&lt;/span&gt;: [],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_type&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;uncoordinated&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        },&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_idx&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_id&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_description&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;move to&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;object_id&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;burner_mjvqii_0&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;manipulating_object_id&amp;#34;&lt;/span&gt;: [],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;spatial_prefix&amp;#34;&lt;/span&gt;: [],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;frame_duration&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ae81ff&#34;&gt;619&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ae81ff&#34;&gt;733&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;mp_ef&amp;#34;&lt;/span&gt;: [],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_type&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;navigation&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        },&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_idx&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_id&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ae81ff&#34;&gt;91&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_description&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;place on next to&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;object_id&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;packing_box_210&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;burner_mjvqii_0&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;chopping_board_211&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;manipulating_object_id&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;packing_box_210&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;spatial_prefix&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;right&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;frame_duration&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ae81ff&#34;&gt;734&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ae81ff&#34;&gt;817&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;mp_ef&amp;#34;&lt;/span&gt;: [],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;skill_type&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;uncoordinated&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        },&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&#34;step-2-generate-stage-aware-ground-truth-labels-per-task&#34;&gt;Step 2: Generate Stage-Aware &amp;ldquo;Ground-Truth&amp;rdquo; Labels (Per-Task)&lt;/h4&gt;&lt;p&gt;This step must be performed &lt;strong&gt;separately for each of your 50 tasks&lt;/strong&gt;.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Isolate Task:&lt;/strong&gt; Get the 200 annotated episodes for &lt;em&gt;one&lt;/em&gt; task (e.g., &amp;ldquo;Preparing Lunch Box&amp;rdquo;).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Calculate Proportions:&lt;/strong&gt; Find the &lt;em&gt;average temporal proportion&lt;/em&gt; of each stage &lt;em&gt;only for that task&amp;rsquo;s 200 episodes&lt;/em&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Create Checkpoints:&lt;/strong&gt; Create the fixed progress checkpoints &lt;em&gt;for that task&lt;/em&gt; (e.g., &lt;code&gt;add_the_chocolate_cookie&lt;/code&gt; ends at 0.15, &lt;code&gt;take_the_bottle_of_tea&lt;/code&gt; ends at 0.7).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Interpolate:&lt;/strong&gt; Generate the new, consistent ground-truth labels for those 200 episodes by linearly interpolating between those task-specific checkpoints.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Repeat:&lt;/strong&gt; Go to the next task and repeat.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;At the end, you have 10,000 episodes with new, consistent labels, where the progress bar is calibrated correctly for its specific task.&lt;/p&gt;&lt;h4 id=&#34;step-3-train-one-multi-task-sarm-model&#34;&gt;Step 3: Train &lt;em&gt;One&lt;/em&gt; Multi-Task SARM Model&lt;/h4&gt;&lt;p&gt;You do &lt;em&gt;not&lt;/em&gt; need to train 50 separate reward models.&lt;/p&gt;&lt;p&gt;Instead, you train &lt;strong&gt;one, single, multi-task SARM model&lt;/strong&gt; on your full 10,000-episode dataset. The key is to make this model &lt;em&gt;task-aware&lt;/em&gt; by feeding it the task instruction (just like a VLA).&lt;/p&gt;&lt;p&gt;Your model&amp;rsquo;s input would be:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Video Frames&lt;/li&gt;&lt;li&gt;&lt;strong&gt;task_id&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The model (with its dual stage/progress heads) will learn to predict the correct stages and progress &lt;em&gt;conditional&lt;/em&gt; on the task instruction.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&amp;ldquo;Rewind Augmentation&amp;rdquo;&lt;/strong&gt; is the special sauce. This is where the model learns how failures look like. Otherwise one could just use the original &amp;ldquo;Ground Truth&amp;rdquo; and it would be simpler.&lt;/p&gt;&lt;h4 id=&#34;step-4-integrating-the-reward-model-with-vla-training&#34;&gt;Step 4: Integrating the reward model with VLA training&lt;/h4&gt;&lt;p&gt;In this section I will describe how the SARM can be used for VLA training in practice.&lt;/p&gt;&lt;p&gt;The paper mentions changing the loss function to incorporate the weight based on the reward for a given window for a training sample, but I had an insight that weighing the gradients for learning can be achieved in asimpler way, by using weighted random sampler to over/under represent the parts of the training set.&lt;/p&gt;&lt;p&gt;So we will use the SARM model to Pre-Generate Weights once &amp;amp; use the weights for the weighted sampler for VLA training.&lt;/p&gt;&lt;p&gt;This section assumes that you are training your VLA on action chunks (e.g., of length 30) and that you have support for a Weighted Sampler that can take per frame weights.&lt;/p&gt;&lt;p&gt;We will create a list of weights for each frame of the dataset assuming a fixed chunk size.&lt;/p&gt;&lt;p&gt;For each task:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Slide your 30 step chunk window (from $t$ to $t+30$).&lt;/li&gt;&lt;li&gt;For each chunk, get the progress delta &lt;strong&gt;by feeding the model the task instruction&lt;/strong&gt;:&lt;ul&gt;&lt;li&gt;$P_{start} = \phi(d_t, task\_id)$&lt;/li&gt;&lt;li&gt;$P_{end} = \phi(d_{t+30}, task\_id)$&lt;/li&gt;&lt;li&gt;$\hat{r} = P_{end} - P_{start}$&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;Map this task-specific $\hat{r}$ to a final, non-negative weight $w$ (e.g., if $\hat{r} \le 0$, $w=0$; else $w=1$)&lt;/li&gt;&lt;li&gt;Store $w$ in your global list of weights.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;After this, you will have a single, massive list of weights, perfectly aligned with your training chunks. You can then feed this into a &lt;code&gt;WeightedRandomSampler&lt;/code&gt; to train your &lt;strong&gt;single, multi-task VLA&lt;/strong&gt; (like $\pi_0$) using the standard BC loss.&lt;/p&gt;&lt;p&gt;No changes to the loss function necessary, pretty much all the changes are in the data loader.&lt;/p&gt;&lt;h3 id=&#34;next-steps--conclusions&#34;&gt;Next steps &amp;amp; conclusions&lt;/h3&gt;&lt;p&gt;I don&amp;rsquo;t have results yet when it comes to using the SARM for the BEHAVIOR challenge yet, fundamentally we don&amp;rsquo;t even really know how good the dataset is already.&lt;/p&gt;&lt;p&gt;Taking better advantage of the datasets we already have is very smart, especially when the demonstrations come from human operators of varied skill, so I expect ideas such as SARM to be relevant for a long time.&lt;/p&gt;&lt;p&gt;The obvious next step having a good Reward Model would be to try to use it with RL, especially if a simulator is present. The paper described an effective way to do it with a diffusion model, but it&amp;rsquo;s not yet clear to me how to effectively translate it to RL training of a complex model such as $\pi_{0.5}$.&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Robotics Hackathon in Bimanual Manipulation in Munich</title>
       <link>https://tinystruggles.com/posts/robotics_hackathon/</link>
       <pubDate>Sun, 05 Oct 2025 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/robotics_hackathon/</guid>
       <description>&lt;p&gt;&lt;em&gt;How a LinkedIn Post Led Me to a Munich Basement with Millions of Euros Worth of Robotics Equipment&lt;/em&gt;&lt;/p&gt;&lt;p&gt;My LinkedIn feed has become a stream of robotics content over the past few months. As someone diving deep into AI robotics after years in ML/AI/RL, I&amp;rsquo;ve been deliberately connecting with people pushing the boundaries of the field. So when Nicolas Keller&amp;rsquo;s post about Munich being &lt;a href=&#34;https://www.linkedin.com/posts/nicolas-m-keller_munich-is-the-worlds-best-place-to-build-activity-7367081527312060416-QRWC/?utm_source=social_share_send&amp;amp;utm_medium=member_desktop_web&amp;amp;rcm=ACoAABLwMh0BFQy6nP8-zfZDAIlEohX2XofGdNM&#34;&gt;&amp;ldquo;the world&amp;rsquo;s best place to build robots&amp;rdquo;&lt;/a&gt; appeared in my feed, it immediately got my attention.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/munich_hackathon_hu_5fcdcb55331c191b.jpeg&#34;&gt;&lt;p&gt;A bimanual manipulation hackathon in Munich, organized in just three weeks. How cool is that?&lt;/p&gt;&lt;p&gt;Here&amp;rsquo;s what they promised:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Hands-on with dual-cobot humanoid upper-body setups, 2 Franka Emika Pandas, 2 depth cameras, 1 rgb, RTX5090 in each station&lt;/li&gt;&lt;li&gt;Teleoperation with Meta Quest VR headsets&lt;/li&gt;&lt;li&gt;Contributing to the MINGA research paper (with co-author potential)&lt;/li&gt;&lt;li&gt;Collaborating in Munich&amp;rsquo;s growing robotics ecosystem&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I had three weeks to apply, get accepted, and arrange travel during what turned out to be Oktoberfest (the Lederhosen on the robot in the picture should have been a hint! It just made the stay extra expensive).&lt;/p&gt;&lt;p&gt;I have recently missed the LeRobot&amp;rsquo;s worldwide hackathon and wanted to jump on the opportunity. The prospect of 30+ dual-arm robot setups, high-end GPUs, industry mentors and meeting other people passionate about robotic manipulation made it a no-brainer decision for me.&lt;/p&gt;&lt;h2 id=&#34;the-hackathon-experience&#34;&gt;The hackathon experience&lt;/h2&gt;&lt;p&gt;Most hackathons are short and they only involve software. Hardware Hackathons are much more rare, especially where hardware is provided and high end.&lt;/p&gt;&lt;p&gt;The organizers promised a lot - and I have to say that they over-delivered, even though they operated on a very short timeline! 3 weeks!&lt;/p&gt;&lt;p&gt;The hackathon wasn&amp;rsquo;t perfect; we hit some technical issues with the provided codebase and the robot controllers.The lab space (KI Fabrik in the Deutsches Museum) was full of amazing robots, powerful workstations, and a 3D workshop, but it also had its downsides - it was hot, humid and you could get trapped there due to the limited number of keys!&lt;/p&gt;&lt;p&gt;The schedule was focused on building: after one day of setup and tutorials, it was essentially &amp;ldquo;09:00–open ended — Building time&amp;rdquo; for six straight days. The main communication happened on Discord.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/schedule_hu_ccb2d7fa706e11e.jpeg&#34;&gt;&lt;p&gt;The setup was industrial-grade: 30+ Franka Emika Panda dual-arm configurations for about 40 participants. Each setup came with Meta Quest headsets running custom teleoperation software. There was a full workshop with 3D printing capabilities for custom grippers. The compute power was serious—workstations with the latest hardware that most of us don&amp;rsquo;t have access to.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/robots_munich_hu_e63664d3a37f8e38.jpg&#34;&gt;&lt;p&gt;The initial goal of the organizers was to attract local students (mostly from TUM), but the hackathon was just too attractive.&lt;/p&gt;&lt;p&gt;The organizers ran the selection process based on a Typeform where you had to justify your presence (CV, motivation, experience) and the final mix of people contained: PhD researchers, startup founders, industry engineers, and ambitious students. There was a significant number of people who traveled from other countries. Everyone wanted to be there.&lt;/p&gt;&lt;h3 id=&#34;industry-engagement-and-realistic-use-cases&#34;&gt;Industry engagement and realistic use cases&lt;/h3&gt;&lt;p&gt;The real differentiator was the industry backing. BMW and Siemens provided realistic challenges to be solved, explained the details, provided physical materials and sponsored prizes.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/siemens_use_case_hu_8d67b2aaaad7a560.png&#34;&gt;&lt;p&gt;Additionally, there were helpful lectures and mentoring from Nvidia, Hugging Face LeRobot and KIT (a big German university).&lt;/p&gt;&lt;p&gt;When Sunday&amp;rsquo;s final presentations came, BMW and Siemens employes showed up to judge the results personally. This wasn&amp;rsquo;t academic theory - teams were working on problems that companies actually need solved, with the decision-makers accessible throughout and present for the final outcomes.&lt;/p&gt;&lt;p&gt;The main organizers were &lt;a href=&#34;https://www.tum.de/&#34;&gt;TUM&lt;/a&gt; and &lt;a href=&#34;https://pokeandwiggle.com/&#34;&gt;Poke &amp;amp; Wiggle&lt;/a&gt;. I was very impressed with both. TUM showed great support for students and entrepreneurship. Poke&amp;amp;Wiggle people pulled everything together from the technical side. They were staying late and even hosted some participants coming from abroad in their homes! They were testing their own software stack while building what they claimed would become the largest public bimanual manipulation dataset.&lt;/p&gt;&lt;h3 id=&#34;my-strategy--learning&#34;&gt;My strategy &amp;amp; Learning&lt;/h3&gt;&lt;p&gt;Probably my favorite thing about this hackathon was that it was extremely collaborative. Yes, people tried to win, but I was able to learn both from my team as well as from others.&lt;/p&gt;&lt;h4 id=&#34;team-formation--collaboration&#34;&gt;Team formation &amp;amp; collaboration&lt;/h4&gt;&lt;p&gt;I came to the event without knowing anyone and needed to form a team. It was actually a pretty common experience, many people didn&amp;rsquo;t know who to pair with. I have been in setups like this before and that experience helped.&lt;/p&gt;&lt;p&gt;To form a good team you want the best people, but also it&amp;rsquo;s really hard to assess people very quickly and people who seem great at the start might not be able to give the best performance. E.g. they might not be fully available, you might not get along very well, etc.&lt;/p&gt;&lt;p&gt;So my strategy was to talk to most people, see how they think and how experienced they are. I was selecting for getting along, enthusiasm and general intelligence. It was less important to me that someone was inexperienced as long as they were energetic and open minded.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/team_hu_ced34c45fe61b8c8.jpg&#34;&gt;&lt;p&gt;We weren&amp;rsquo;t the most effective or best organized, but we really enjoyed our time together, made good progress and learned a lot. Our team also shifted a bit during the 7 days (one person got sick and we adopted another one).&lt;/p&gt;&lt;p&gt;We used a WhatsApp group to share resources, set up a GitHub repo for shared scripts and shared some notes on a Google doc.&lt;/p&gt;&lt;p&gt;Our strategy was as follows:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;get familiar with the teleoperation&lt;/li&gt;&lt;li&gt;trying out all available tasks and assess the task feasibility for the human operators&lt;/li&gt;&lt;li&gt;train and deploy the models ASAP to test the pipeline (yes, we detected bugs and further limitations)&lt;/li&gt;&lt;li&gt;pursue the most promising tasks, refine the dataset collection and experiment with the models for good performance&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;learnings&#34;&gt;Learnings&lt;/h3&gt;&lt;p&gt;We didn&amp;rsquo;t manage to win any categories. I think that my group was more focused on learning and experimentation, instead of purely competing to win.&lt;/p&gt;&lt;p&gt;Some takeaways:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;If the task can&amp;rsquo;t be done by the human, the robot won&amp;rsquo;t be able to do it&lt;/li&gt;&lt;li&gt;Training loss during model training is not a good predictor of the real life performance&lt;/li&gt;&lt;li&gt;Robot safety mechanisms were critical to avoid breaking the robot&lt;/li&gt;&lt;li&gt;Evaluation in real life is risky and some simulation setup would be helpful&lt;/li&gt;&lt;li&gt;End-effector control with inverse kinematics was often causing the robots to get stuck due to joint limits; it required special care during teleoperation for the demos so that the actual policy wouldn&amp;rsquo;t block the robot&lt;/li&gt;&lt;li&gt;Two arms are much harder than one&lt;/li&gt;&lt;li&gt;Dataset quality matters a lot (recovery examples, non-Markovian states are confusing, noise/operators in the setup)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;What we tested&lt;/p&gt;&lt;ul&gt;&lt;li&gt;3 different task setups with hundred+ demonstrations each&lt;/li&gt;&lt;li&gt;variations in model training (steps, parameters, models, action space) and dataset selection (recovery episodes ratio, bad episodes)&lt;/li&gt;&lt;li&gt;different control modes (delta and absolute)&lt;/li&gt;&lt;li&gt;we could only try actions in the EE space, the joint space controllers weren&amp;rsquo;t working correctly&lt;/li&gt;&lt;li&gt;SMOLVLA and ACT models from Lerobot libraries&lt;/li&gt;&lt;li&gt;performed experiments for generalization and resilience (e.g. messing with cameras was making robots much less effective) we also visualized attention maps for the ACT Models&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Here is an example of an attention map:&lt;img src=&#34;https://tinystruggles.com/heatmap.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;&lt;p&gt;(Kudos to &lt;a href=&#34;https://github.com/villekuosmanen/physical-AI-interpretability&#34;&gt;physical AI Interpretability Repo&lt;/a&gt; - the author was there during the event and helped us a bit with the setup).&lt;/p&gt;&lt;p&gt;Misc&lt;/p&gt;&lt;ul&gt;&lt;li&gt;I got a pretty good feel for teleoperation in VR (it&amp;rsquo;s hard though!)&lt;/li&gt;&lt;li&gt;I managed to get the arms to crash with each other and got my robots stuck countless times&lt;/li&gt;&lt;li&gt;I spent some time setting up a simulation environment with Panda in MuJoCo and playing with Isaac Sim (approach abandoned in the end)&lt;/li&gt;&lt;li&gt;I read several papers recommended by other participants&lt;/li&gt;&lt;li&gt;I wasn&amp;rsquo;t able to try Pi0/Pi0.5 as the ready snapshots are for the same type of robot (Panda), but in a very different action space and we didn&amp;rsquo;t have time/resources for fine-tuning from scratch (80GB+ GPU memory required)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;What I wished I could do:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;try out Groot / Pi0.5&lt;/li&gt;&lt;li&gt;simulation, sim-to-real, and RL fine-tuning for the trained VLA (a simple VLA would be perfect!)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Posts describing the experiences of some of my teammates: &lt;a href=&#34;https://www.linkedin.com/feed/update/urn:li:activity:7376365518649393152/&#34;&gt;@Artur&lt;/a&gt; and &lt;a href=&#34;https://www.linkedin.com/feed/update/urn:li:activity:7376314676193910786/&#34;&gt;@Andrea&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&#34;we-need-more-of-this&#34;&gt;We Need More of This&lt;/h2&gt;&lt;p&gt;After seven days of intense collaboration in a basement, we didn&amp;rsquo;t revolutionize the future of robotics, but we all learned a lot and everyone came back home more experienced and inspired.&lt;/p&gt;&lt;p&gt;It was a great event!&lt;/p&gt;&lt;p&gt;And I want to see more events like this in Europe, because the future of robotics doesn&amp;rsquo;t have to happen in SFO (or China)&lt;/p&gt;&lt;p&gt;Europe has the ingredients for world-class robotics innovation. We have strong engineering talent and strong reasons to invest (aging population)! I live in Poland now, a place that produces some of the world&amp;rsquo;s best software engineers, but the innovation is lacking.  I talked to two universities in Warsaw and they don&amp;rsquo;t really innovate or even follow the current state of the art yet for embodied intelligence.&lt;/p&gt;&lt;p&gt;And it&amp;rsquo;s a shame, because with libraries such as LeRobot, open hardware, and open-source simulation engines, the space is now much more accessible.&lt;/p&gt;&lt;p&gt;I was very impressed with TUM and many of the students. I would like to support the ecosystem in Poland and Europe. Munich proved it&amp;rsquo;s possible. Let me know if you would like to help!&lt;/p&gt;&lt;h2 id=&#34;next-steps&#34;&gt;Next steps&lt;/h2&gt;&lt;p&gt;I left the event pretty drained, but also very excited!  JI am still following up on the various threads I started during the hackathon. I also started to look at another &lt;a href=&#34;https://behavior.stanford.edu/index.html&#34;&gt;exciting challenge&lt;/a&gt; that is focused on household tasks in the simulation.&lt;/p&gt;&lt;p&gt;Currently I&amp;rsquo;m especially interested in the approaches combining VLAs with RL, like the ones outlined in the &lt;a href=&#34;https://arxiv.org/abs/2509.09674&#34;&gt;SimpleVLA paper&lt;/a&gt; and would love to participate in more hardware hackathons, ideally combining simulation and real world learning.&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Robotics Training Experiments: What Worked, What Didn&#39;t, and What Surprised Me</title>
       <link>https://tinystruggles.com/posts/robotics_ml_training/</link>
       <pubDate>Sun, 17 Aug 2025 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/robotics_ml_training/</guid>
       <description>&lt;p&gt;&lt;em&gt;Part 2 of a series on practical robotics experimentation&lt;/em&gt;. See &lt;a href=&#34;https://www.tinystruggles.com/posts/building_robotics_playground/&#34;&gt;part1&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;After building my simulation environment, the real learning began. I threw multiple approaches at the cube-to-bin task: Soft Actor-Critic (SAC) reinforcement learning, behavior cloning from demonstrations, and modern imitation learning with LeRobot. Each taught me something different about the practical realities of robot learning.&lt;/p&gt;&lt;p&gt;Here&amp;rsquo;s what I discovered—including some findings that challenged my assumptions and revealed the gap between academic ideals and development reality.&lt;/p&gt;&lt;p&gt;You can find the training &lt;a href=&#34;https://github.com/ilonajulczuk/gym-so100-c/tree/main/scripts&#34;&gt;code here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&#34;why-sac-guidance-from-chatgpt-and-the-literature&#34;&gt;Why SAC? Guidance from ChatGPT and the Literature&lt;/h2&gt;&lt;p&gt;When it came time to choose a reinforcement learning algorithm for manipulation, I turned to ChatGPT for guidance on what would work well for the precise, continuous control that robotic manipulation demands.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The Algorithm Landscape:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;PPO&lt;/strong&gt;: Great for many RL tasks, but can struggle with the precise, smooth actions needed for manipulation&lt;/li&gt;&lt;li&gt;&lt;strong&gt;DDPG&lt;/strong&gt;: Deterministic policy gradients, but known for training instability&lt;/li&gt;&lt;li&gt;&lt;strong&gt;TD3&lt;/strong&gt;: Improved version of DDPG with better stability&lt;/li&gt;&lt;li&gt;&lt;strong&gt;SAC&lt;/strong&gt;: Soft Actor-Critic with entropy regularization&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Why SAC Emerged as the Right Choice:&lt;/strong&gt;Through my conversations with ChatGPT and reading the literature, SAC stood out for manipulation tasks:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Smooth, continuous actions&lt;/strong&gt;: Essential for robotic control&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Sample efficiency&lt;/strong&gt;: Better than policy gradient methods like PPO&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Stability&lt;/strong&gt;: More reliable than DDPG/TD3 for continuous control&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Exploration&lt;/strong&gt;: The entropy term helps discover diverse manipulation strategies&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;The Manipulation Sweet Spot:&lt;/strong&gt;Robotic manipulation requires policies that can be both &lt;strong&gt;exploratory&lt;/strong&gt; (to discover grasping strategies) and &lt;strong&gt;precise&lt;/strong&gt; (for fine motor control). SAC&amp;rsquo;s soft policy approach naturally balances these needs.&lt;/p&gt;&lt;p&gt;My background in ML and deep generative modeling helped me understand why the entropy regularization was important, but ChatGPT&amp;rsquo;s domain-specific guidance really pointed me toward SAC as the practical choice for this type of problem.&lt;/p&gt;&lt;p&gt;This background influenced my entire approach to the experiments that followed.&lt;/p&gt;&lt;p&gt;You&amp;rsquo;re absolutely right! I made the same mistake again. Let me fix that section:&lt;/p&gt;&lt;h2 id=&#34;the-sac-experiments-dense-vs-sparse-rewards&#34;&gt;The SAC Experiments: Dense vs. Sparse Rewards&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;The Reward Shaping Struggle:&lt;/strong&gt;I didn&amp;rsquo;t initially know about HER. I first spent several days training and tweaking reward shaping—trying sparse and dense methods, experimenting with penalizing for missing the goal on every step versus not penalizing. When I saw poor training success and weird behaviors from my trained agents, it made me investigate alternative strategies.&lt;/p&gt;&lt;h3 id=&#34;sac-with-dense-rewards-the-winner&#34;&gt;SAC with Dense Rewards: The Winner&lt;/h3&gt;&lt;p&gt;For dense rewards, I shaped the reward function around manipulation primitives:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Simplified reward structure&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;approach_reward &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;distance_to_cube&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;grasp_reward &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; gripper_closure_bonus &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; near_cube &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lift_reward &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; cube_height_above_table &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; lift_multiplier&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;placement_reward &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;distance_cube_to_bin &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; cube_grasped &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;success_reward &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; cube_in_bin &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This approach worked reliably. The policy learned smooth approach trajectories, consistent grasping, and coordinated placement motions. Training was stable and converged predictably.&lt;/p&gt;&lt;h3 id=&#34;sac--her-with-sparse-rewards-a-later-discovery&#34;&gt;SAC + HER with Sparse Rewards: A Later Discovery&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Why HER Seemed Appealing:&lt;/strong&gt;When I discovered HER, it promised a more disciplined approach to the reward engineering problem I&amp;rsquo;d been struggling with: &lt;strong&gt;just define success and let the algorithm figure out the intermediate steps&lt;/strong&gt;. The idea is elegant—when an episode fails to reach the intended goal, HER treats it as if it was trying to reach wherever it actually ended up. This creates synthetic &amp;ldquo;success&amp;rdquo; experiences from every failed attempt.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The HER Insight:&lt;/strong&gt;Instead of manually crafting rewards for &amp;ldquo;getting closer to the cube,&amp;rdquo; HER automatically discovers that reaching certain intermediate positions is useful for eventually reaching the final goal. It&amp;rsquo;s like learning to play chess by treating every game as a success for reaching whatever position you actually achieved.&lt;/p&gt;&lt;p&gt;For robotic manipulation, this seemed perfect:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;No manual reward engineering&lt;/strong&gt;: Just define task success&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Automatic curriculum&lt;/strong&gt;: Learn easier goals (approach) before harder ones (grasp+place)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Sample efficiency&lt;/strong&gt;: Every episode contributes training signal&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;The Implementation Reality:&lt;/strong&gt;But first, I had to figure out &lt;strong&gt;how to actually implement HER&lt;/strong&gt;, which wasn&amp;rsquo;t obvious. The documentation and examples didn&amp;rsquo;t make it clear how to structure the environment properly.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The Goal Environment Challenge:&lt;/strong&gt;HER requires a &amp;ldquo;goal-conditioned&amp;rdquo; environment where observations include both the current state and a target goal. See my implementation &lt;a href=&#34;https://github.com/ilonajulczuk/gym-so100-c/blob/main/gym_so100/env.py#L188&#34;&gt;example&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The HER Training Setup:&lt;/strong&gt;Once you have the goal environment structure:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; stable_baselines3 &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; HerReplayBuffer, SAC&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;model &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; SAC(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;MultiInputPolicy&amp;#34;&lt;/span&gt;,  &lt;span style=&#34;color:#75715e&#34;&gt;# Required for dict observations&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    env,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    replay_buffer_class&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;HerReplayBuffer,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    replay_buffer_kwargs&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;dict(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        n_sampled_goal&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;4&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        goal_selection_strategy&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;future&amp;#39;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    verbose&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;The honest truth about my experience:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Learning progress was painfully slow&lt;/strong&gt;—each step of improvement took much longer compared to dense rewards&lt;/li&gt;&lt;li&gt;&lt;strong&gt;I never actually had the patience to let it finish training&lt;/strong&gt;—the progress was so glacial that I always gave up and went back to dense rewards&lt;/li&gt;&lt;li&gt;&lt;strong&gt;The few times I did wait longer&lt;/strong&gt;, performance was unstable&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Was I doing it right?&lt;/strong&gt; It&amp;rsquo;s entirely possible that I wasn&amp;rsquo;t implementing the GoalEnv correctly, or that my hardware was too slow for HER to show its benefits. The implementation wasn&amp;rsquo;t obvious from the documentation, and debugging goal-conditioned environments is more complex than standard ones.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;For the patient (or well-equipped):&lt;/strong&gt; If you have more patience than I did—or faster hardware—this goal-conditioned setup might work well for you. But life is short and iteration speed matters.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Academic papers often focus on sample efficiency, but wall-clock time and learning progress rate matter enormously for practical development. Sometimes the &amp;ldquo;less elegant&amp;rdquo; dense reward approach is the right engineering choice.&lt;/p&gt;&lt;h2 id=&#34;the-sac-training-setup-real-implementation-details&#34;&gt;The SAC Training Setup: Real Implementation Details&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s what my actual SAC training pipeline looked like (full implementation in &lt;a href=&#34;https://github.com/ilonajulczuk/gym-so100-c/blob/main/scripts/train_sac.py&#34;&gt;&lt;code&gt;scripts/train_sac.py&lt;/code&gt;&lt;/a&gt;):&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Multi-Environment Training:&lt;/strong&gt;&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Create vectorized environments for faster training&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;vec_env &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; make_vec_env(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    create_single_env,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    n_envs&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;num_envs,  &lt;span style=&#34;color:#75715e&#34;&gt;# I used 2-6 parallel environments&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    vec_env_cls&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;SubprocVecEnv,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    env_kwargs&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;task&amp;#34;&lt;/span&gt;: task},&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;vec_env &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; VecTransposeImage(vec_env)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;vec_env &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; VecNormalize(vec_env, norm_obs&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;, norm_reward&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;False&lt;/span&gt;, clip_obs&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;10.0&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Model Configuration:&lt;/strong&gt;&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;model &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; SAC(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    policy&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;MultiInputPolicy&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    env&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;vec_env,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    learning_rate&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1e-4&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    buffer_size&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;2_000&lt;/span&gt;,  &lt;span style=&#34;color:#75715e&#34;&gt;# Started small, later increased&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    batch_size&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;256&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ent_coef&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;auto&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    target_entropy&lt;span style=&#34;color:#f92672&#34;&gt;=-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;2.0&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    device&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;device,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    tensorboard_log&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;log_dir,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Stage-Based Training Approach:&lt;/strong&gt;Rather than just running continuous training, I implemented a stage-based system that adjusted exploration over time:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;StageBasedTraining&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;__init__&lt;/span&gt;(self, model, vec_env, callback&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;, start_steps&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, num_envs&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# Stage 1: High exploration (target_entropy = -2.0)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# Stage 2: Balanced phase (target_entropy = -3.0) &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# Stage 3: Exploitation (target_entropy = -7.0, lower learning rate)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Custom Evaluation Callback:&lt;/strong&gt;The most useful part was a custom callback that recorded videos during training:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;EvaluationVideoCallback&lt;/span&gt;(BaseCallback):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_on_step&lt;/span&gt;(self) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; bool:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;eval_freq &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;n_calls &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;eval_freq &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            mean_reward, video_frames &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;evaluate_function(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;model, num_episodes&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;num_episodes&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            )&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# Save video and update best model if improved&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This let me visually track learning progress and automatically save the best-performing models with videos.&lt;/p&gt;&lt;h2 id=&#34;the-stability-challenge-and-practical-constraints&#34;&gt;The Stability Challenge and Practical Constraints&lt;/h2&gt;&lt;p&gt;Even with dense rewards, SAC wasn&amp;rsquo;t always smooth sailing. I encountered several issues that taught me about the practical side of RL:&lt;/p&gt;&lt;h3 id=&#34;the-buffer-size-discovery-an-accidental-experiment&#34;&gt;The Buffer Size Discovery: An Accidental Experiment&lt;/h3&gt;&lt;p&gt;Looking back at my code, I discovered an interesting inconsistency that taught me about buffer sizes the hard way:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Fresh training: very small buffer&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;buffer_size&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;2_000&lt;/span&gt;,  &lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Resumed training: much larger buffer  &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;buffer_size&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;50_000&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This &lt;strong&gt;25x difference&lt;/strong&gt; was completely unintentional, but it created an accidental experiment. The small buffer meant:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Faster iteration&lt;/strong&gt; during initial experimentation&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Lower memory usage&lt;/strong&gt; on my M3 MacBook (I was easily hitting memory limits)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Less stable training&lt;/strong&gt; due to limited experience replay&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;The M3 Memory Reality:&lt;/strong&gt;On macOS M3, I was constantly running into memory constraints. The 2K buffer was actually a practical necessity rather than a design choice—larger buffers would cause memory issues, especially with multiple vectorized environments and image observations.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Episode Length Evolution:&lt;/strong&gt;I initially started with 300-step episodes, but later extended them to 700 steps. The reason: &lt;strong&gt;300 steps were too short for me to complete teleoperation demonstrations&lt;/strong&gt;. This change also affected my buffer size calculations—longer episodes meant each episode took more buffer space, so my effective &amp;ldquo;number of episodes in buffer&amp;rdquo; was different than I initially thought.&lt;/p&gt;&lt;h3 id=&#34;platform-dependencies-the-colab-surprise&#34;&gt;Platform Dependencies: The Colab Surprise&lt;/h3&gt;&lt;p&gt;Stable-Baselines3&amp;rsquo;s vectorized environments were genuinely useful for throughput, but there was a &lt;strong&gt;huge platform dependency&lt;/strong&gt;: On macOS with MuJoCo running on CPU, vectorized environments were surprisingly fast. On Google Colab, the same setup was painfully slow—likely due to weaker single-thread CPU performance and different MuJoCo builds.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Always benchmark on your target platform. Performance characteristics can vary dramatically.&lt;/p&gt;&lt;h3 id=&#34;the-real-development-workflow-storage-videos-and-ai-guided-decisions&#34;&gt;The Real Development Workflow: Storage, Videos, and AI-Guided Decisions&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;The Storage Reality:&lt;/strong&gt;I was checkpointing aggressively—saving models, replay buffers, and VecNormalize stats every few thousand steps. Between the checkpoints and training videos, I ran out of my 1TB MacBook storage multiple times. The main culprit was the &lt;strong&gt;replay buffers at 3-4GB each&lt;/strong&gt;—pretty typical for SAC with image observations. The other files were small in comparison:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Model checkpoints (~100MB each)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Replay buffers (3-4GB each - the real storage killer)&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;VecNormalize stats (small)&lt;/li&gt;&lt;li&gt;Evaluation videos (small)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;When you&amp;rsquo;re saving every few thousand steps and keeping multiple checkpoints for safety, those 3-4GB replay buffers add up fast.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Videos as the Primary Feedback Loop:&lt;/strong&gt;The evaluation videos turned out to be absolutely critical for understanding training progress. Unlike the reward curves, videos immediately showed me:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Whether the robot was learning useful behaviors&lt;/li&gt;&lt;li&gt;If it was getting stuck in local minima (like endlessly circling the cube)&lt;/li&gt;&lt;li&gt;When training was diverging before the metrics made it obvious&lt;/li&gt;&lt;li&gt;The quality of grasping and placement behaviors&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Divergence Patterns in Video:&lt;/strong&gt;Videos were especially revealing when the model diverged. You could see the policy &lt;strong&gt;just curling up in some specific pose regardless of what was happening&lt;/strong&gt;—the robot would move to the same configuration every time, completely ignoring the cube position or any environmental state. This was much more obvious in video than in the reward curves, which might still show some variance.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Visualizing Entropy:&lt;/strong&gt;The videos also worked as an excellent &lt;strong&gt;entropy visualizer&lt;/strong&gt;. When entropy was too high, you&amp;rsquo;d see the robot making erratic, inconsistent movements—approaching the cube from random angles, jerky motions, inconsistent grasping attempts. When entropy was too low, the robot would be overly deterministic, potentially getting stuck in the same suboptimal strategy repeatedly.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;AI-Assisted Training Decisions:&lt;/strong&gt;I regularly copied my tensorboard training stats and shared them with Claude to get guidance on whether to interrupt training and resume from an earlier checkpoint. Since I didn&amp;rsquo;t know exactly what learning rate and entropy target to set for SAC, having an AI co-pilot helped me interpret the training curves and decide when to intervene.&lt;/p&gt;&lt;p&gt;This back-and-forth with Claude became part of my training workflow—especially when deciding whether unstable training was worth continuing or if I should revert to a more stable checkpoint.&lt;/p&gt;&lt;h2 id=&#34;the-resolution-debugging-story&#34;&gt;The Resolution Debugging Story&lt;/h2&gt;&lt;p&gt;This was my biggest &amp;ldquo;check your assumptions&amp;rdquo; moment. I had been training with &lt;strong&gt;64×48 pixel observations&lt;/strong&gt; for speed, and the results were disappointing. Policies would approach the cube but fail at precise manipulation.&lt;/p&gt;&lt;p&gt;I spent weeks tuning hyperparameters, reward functions, and network architectures. The rendered videos looked fine—I could clearly see the cube and bin. What was wrong?&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The breakthrough came when I examined the actual tensor inputs to the model:&lt;/strong&gt;&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Debug: save actual model inputs&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;obs_tensor &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; env&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;observation_space&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sample()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;plt&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;imshow(obs_tensor[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;pixels&amp;#39;&lt;/span&gt;]&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;transpose(&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;))&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;plt&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;title(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;What the model actually sees&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;plt&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;show()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The 64×48 images were severely degraded compared to my 480×640 rendered videos. Critical details for grasping—cube edges, gripper alignment, depth perception—were lost in the downsampling.&lt;/p&gt;&lt;p&gt;Switching to 640x480 input resolution immediately improved performance. &lt;strong&gt;The lesson:&lt;/strong&gt; Always verify your actual model inputs, not just what you think you&amp;rsquo;re feeding it. Rendered videos can be misleading about what the policy actually observes.&lt;/p&gt;&lt;h2 id=&#34;behavior-cloning-the-simulation-vs-reality-gap&#34;&gt;Behavior Cloning: The Simulation vs. Reality Gap&lt;/h2&gt;&lt;p&gt;I also experimented with imitation learning using manually collected demonstrations (implementation in &lt;a href=&#34;https://github.com/ilonajulczuk/gym-so100-c/blob/main/scripts/train_bc.py&#34;&gt;&lt;code&gt;scripts/train_bc.py&lt;/code&gt;&lt;/a&gt;). The behavior cloning itself worked well—policies could replicate demonstrated behaviors with reasonable fidelity. But collecting quality demonstrations revealed a surprising insight.&lt;/p&gt;&lt;p&gt;Using keyboard or gamepad controls, I struggled to actually complete the task successfully. &lt;strong&gt;I would succeed maybe 1 out of 5 attempts&lt;/strong&gt; in simulation, spending most episodes fumbling with controls and failing to grasp or place the cube properly.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The insight that changed everything:&lt;/strong&gt; Later, when I got access to a leader arm for real-world teleoperation, the difference was stark. &lt;strong&gt;Tasks I could barely complete in simulation became trivial in reality&lt;/strong&gt;—I could consistently complete the manipulation sequence on nearly every attempt.&lt;/p&gt;&lt;p&gt;This isn&amp;rsquo;t about speed—it&amp;rsquo;s about &lt;strong&gt;task completion rate&lt;/strong&gt;. The same human (me) went from a ~20% success rate in simulation to near-100% success rate with physical teleoperation.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The Double-Edged Sword:&lt;/strong&gt;This creates an interesting tension in imitation learning. Struggling with simulation controls means you&amp;rsquo;re training on lower-quality demonstrations, but &lt;strong&gt;imperfect demonstrations might actually be better for learning robust policies&lt;/strong&gt;—perfect demonstrations can be brittle and don&amp;rsquo;t show recovery behaviors.&lt;/p&gt;&lt;p&gt;My simulation demos included:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Recovery behaviors when grasps failed&lt;/li&gt;&lt;li&gt;Multiple approach angles when obvious paths didn&amp;rsquo;t work&lt;/li&gt;&lt;li&gt;Natural variation in timing and trajectories&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Future Improvements: Hybrid Demonstration Strategies&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Looking ahead, I could significantly improve the setup:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;IK + Noise Demonstrations:&lt;/strong&gt;Generate synthetic demonstrations using inverse kinematics with added noise for volume and consistency, following the ALOHA approach.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Leader Arm → Simulation Bridge:&lt;/strong&gt;Connect a physical leader arm directly to simulation—combining intuitive 6-DOF control with simulation benefits.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Hybrid Dataset Strategy:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;IK + noise for bulk demonstrations with good coverage&lt;/li&gt;&lt;li&gt;Human struggle for recovery behaviors and natural variation&lt;/li&gt;&lt;li&gt;Leader arm sim for high-quality expert demonstrations&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Each source contributes different aspects of robust manipulation behavior.&lt;/p&gt;&lt;h2 id=&#34;vecnormalize-mixed-feelings&#34;&gt;VecNormalize: Mixed Feelings&lt;/h2&gt;&lt;p&gt;Stable-Baselines3 offers &lt;code&gt;VecNormalize&lt;/code&gt; to automatically normalize observations and rewards, but it couples environment statistics with your trained model. Deployment becomes more complex—you need to save normalization statistics alongside the policy.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;My current preference:&lt;/strong&gt; Handle normalization inside the policy network rather than as environment wrappers. It&amp;rsquo;s more explicit and avoids deployment surprises.&lt;/p&gt;&lt;h2 id=&#34;what-id-do-differently&#34;&gt;What I&amp;rsquo;d Do Differently&lt;/h2&gt;&lt;p&gt;Looking back on these experiments, several lessons stand out:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Verify your actual model inputs early&lt;/strong&gt;—don&amp;rsquo;t trust that preprocessing works as expected&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Checkpoint often&lt;/strong&gt;— training is slow, you don&amp;rsquo;t want to lose progress, and it&amp;rsquo;s sometimes helpful to revert back and change some hyperparameters (e.g. to avoid divergence)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Visualize everything&lt;/strong&gt;—videos reveal problems hours before metrics do&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Track your experiments and settings meticulously&lt;/strong&gt;—there are settings scattered everywhere&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;&lt;strong&gt;The Settings Sprawl Problem:&lt;/strong&gt;One of the biggest challenges was keeping track of all the different settings across multiple places:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Reward shaping&lt;/strong&gt;: Defined in the task implementation&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Camera resolution&lt;/strong&gt;: Set in the environment creation&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Environment setup&lt;/strong&gt;: Goal vs. standard env, with or without wrappers&lt;/li&gt;&lt;li&gt;&lt;strong&gt;SAC training parameters&lt;/strong&gt;: Learning rates, buffer sizes, entropy targets&lt;/li&gt;&lt;li&gt;&lt;strong&gt;VecNormalize settings&lt;/strong&gt;: Observation normalization parameters&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;When experiments went wrong, it was often unclear which of these many settings was the culprit. Better experiment tracking from the start would have saved significant debugging time.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The Hyperparameter Uncertainty:&lt;/strong&gt;Honestly, I didn&amp;rsquo;t know exactly what learning rates and entropy targets to set for SAC. Most of my hyperparameter choices came from a combination of:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Default values from Stable-Baselines3&lt;/li&gt;&lt;li&gt;Advice from Claude/ChatGPT&lt;/li&gt;&lt;li&gt;Trial and error based on video feedback&lt;/li&gt;&lt;li&gt;Academic papers (when I could find relevant ones)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This uncertainty made the video &amp;amp; checkpoint system even more valuable—I could experiment with different settings and revert when things went wrong.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;Next up: How I integrated with LeRobot and what I learned about bridging different ML ecosystems in robotics.&lt;/em&gt;&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;Have you experienced similar challenges with RL stability or the simulation vs. real-world gap? What&amp;rsquo;s been your experience with different learning approaches?&lt;/em&gt;&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Creating a Robotics Experimentation Environment: My Experience and Practical Lessons</title>
       <link>https://tinystruggles.com/posts/building_robotics_playground/</link>
       <pubDate>Wed, 13 Aug 2025 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/building_robotics_playground/</guid>
       <description>&lt;p&gt;&lt;em&gt;Part 1 of a series on practical robotics experimentation&lt;/em&gt;&lt;/p&gt;&lt;p&gt;As part of my &lt;a href=&#34;https://www.tinystruggles.com/posts/robotics_journey_1/&#34;&gt;journey into robotics&lt;/a&gt;, I found myself facing a classic problem: I wanted to experiment with different learning approaches for robotic manipulation, but I needed a flexible playground where I could quickly test ideas without being locked into any single framework or workflow.&lt;/p&gt;&lt;p&gt;The result is &lt;a href=&#34;https://github.com/ilonajulczuk/gym-so100-c&#34;&gt;gym-so100-c&lt;/a&gt;, a simulation environment built around the &lt;a href=&#34;https://github.com/TheRobotStudio/SO-ARM100&#34;&gt;Standard Open Arm SO101&lt;/a&gt; that bridges multiple machine learning libraries—Stable-Baselines3, the &lt;code&gt;imitation&lt;/code&gt; library, and Hugging Face&amp;rsquo;s LeRobot—all in one cohesive environment.&lt;/p&gt;&lt;h2 id=&#34;the-sim-first-decision&#34;&gt;The &amp;ldquo;Sim-First&amp;rdquo; Decision&lt;/h2&gt;&lt;p&gt;Even though I had access to physical robots, I chose a &lt;strong&gt;sim-first&lt;/strong&gt; approach for a simple reason: I&amp;rsquo;m more of a software person who enjoys the comfort of my home office and the flexibility to keep working while traveling, rather than spending long days in a lab.&lt;/p&gt;&lt;p&gt;This decision shaped everything about the project. I needed a simulation that was:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Physically realistic&lt;/strong&gt; enough to eventually transfer to hardware&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Fast enough&lt;/strong&gt; for thousands of training episodes&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Flexible enough&lt;/strong&gt; to work with different learning paradigms&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Simple enough&lt;/strong&gt; that I could understand and modify every component&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;why-build-another-gym-environment&#34;&gt;Why Build Another Gym Environment?&lt;/h2&gt;&lt;p&gt;You might wonder: why not just use an existing simulation? I wanted something both flexible and reflecting my hardware platform well in simulation. The robotics world offers many compelling options.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The Simulation Landscape I Considered:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://developer.nvidia.com/isaac/sim&#34;&gt;&lt;strong&gt;Isaac Sim&lt;/strong&gt;&lt;/a&gt; was tempting—NVIDIA&amp;rsquo;s powerhouse with photorealistic rendering and advanced physics. But it requires a proper GPU setup, and I wanted to start experimenting immediately rather than waiting for hardware upgrades.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://www.maniskill.ai/&#34;&gt;&lt;strong&gt;ManiSkill&lt;/strong&gt;&lt;/a&gt; is an exciting newer option with great task diversity and modern ML integration. I&amp;rsquo;m actually quite excited to try this next—it seems to hit the sweet spot of realism and ease of use.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gazebo/ROS&lt;/strong&gt; represents the traditional robotics stack: mature, well-supported, with endless plugins. But the learning curve felt steep for someone coming from a pure ML background, and I wanted to focus on learning algorithms rather than robotics middleware.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;PyBullet&lt;/strong&gt; similar to MuJoCo. Also popular in RL enviornments.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why I Chose MuJoCo + gym-aloha:&lt;/strong&gt;The answer came down to immediate productivity. I could adapt gym-aloha and start experimenting within days, not weeks. It&amp;rsquo;s proven in the papers I was trying to replicate, has excellent contact modeling for manipulation, and enjoys a large ecosystem of compatible tools.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Gym Interface Standard:&lt;/strong&gt;OpenAI Gym (now &lt;a href=&#34;https://gymnasium.farama.org/&#34;&gt;Gymnasium&lt;/a&gt;) defines a standard interface that every reinforcement learning environment implements:&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;obs, info &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; env&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;reset()           &lt;span style=&#34;color:#75715e&#34;&gt;# Start a new episode&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;obs, reward, terminated, truncated, info &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; env&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;step(action)  &lt;span style=&#34;color:#75715e&#34;&gt;# Take an action&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This simple interface is incredibly powerful because it means the &lt;strong&gt;same environment&lt;/strong&gt; can work with:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;RL libraries like Stable-Baselines3 (SAC, PPO, HER&amp;hellip;)&lt;/li&gt;&lt;li&gt;Imitation learning libraries like &lt;code&gt;imitation&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Custom training loops or evaluation pipelines&lt;/li&gt;&lt;li&gt;Any future framework that follows the standard&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;The Evolution Plan:&lt;/strong&gt;This environment is just the beginning. As I move toward more realistic scenarios, I&amp;rsquo;ll likely migrate to Isaac Sim or ManiSkill. But for rapid prototyping and algorithm comparison, this MuJoCo setup has been perfect.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; There are many good options. If your requirements are not very specific, look for something popular and that has something similar to what you need that you can quickly adapt.&lt;/p&gt;&lt;p&gt;This flows naturally from the question and sets up the technical details that follow.&lt;/p&gt;&lt;h2 id=&#34;standing-on-the-shoulders-of-aloha&#34;&gt;Standing on the Shoulders of ALOHA&lt;/h2&gt;&lt;p&gt;Rather than building from scratch, I adapted the &lt;a href=&#34;https://github.com/huggingface/gym-aloha&#34;&gt;gym-aloha&lt;/a&gt; project, which implements the dual-arm ALOHA platform used in several influential imitation learning papers.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The ALOHA Foundation:&lt;/strong&gt;ALOHA (A Low-cost Open Hardware Arm) proved that effective manipulation learning was possible with relatively simple hardware. The gym-aloha implementation provided:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;MuJoCo physics foundation&lt;/li&gt;&lt;li&gt;Well-designed observation and action spaces&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;My Adaptation:&lt;/strong&gt;I modified gym-aloha for a &lt;strong&gt;single SO101 arm&lt;/strong&gt; (5-DOF + gripper) to match my hardware target:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Simple registration example&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;register(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    id&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;gym_so100/SO100CubeToBin-v0&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    entry_point&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;gym_so100.env:SO100Env&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    max_episode_steps&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;700&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    nondeterministic&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    kwargs&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;obs_type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;so100_pixels_agent_pos&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;task&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;so100_cube_to_bin&amp;#34;&lt;/span&gt;},&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once registered, creating the environment is straightforward:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; gym_so100               &lt;span style=&#34;color:#75715e&#34;&gt;# triggers env registration&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; gymnasium &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; gym&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;env &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; gym&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;make(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;gym_so100/SO100CubeToBin-v0&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;obs, info &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; env&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;reset()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-task-cube-to-bin&#34;&gt;The Task: Cube-to-Bin&lt;/h2&gt;&lt;p&gt;I focused on one fundamental manipulation task: &lt;strong&gt;bin-a-cube&lt;/strong&gt;. A red cube starts at a random position on the table, and the goal is to place it inside a fixed gray bin.&lt;/p&gt;&lt;p&gt;This task is deceptively simple but covers the core challenges of manipulation:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Perception&lt;/strong&gt;: Locating the cube and understanding spatial relationships&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Planning&lt;/strong&gt;: Approaching the cube from a graspable angle&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Control&lt;/strong&gt;: Executing smooth, coordinated motion&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Manipulation&lt;/strong&gt;: Grasping, lifting, and precise placement&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The MuJoCo scene includes:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Robot&lt;/strong&gt;: SO101 single arm with position-controlled actuators&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Workspace&lt;/strong&gt;: Table, free-moving cube, and goal bin&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Sensors&lt;/strong&gt;: Joint positions, gripper state, and camera views&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Sites&lt;/strong&gt;: Tracking points for reward computation and success detection&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;control-paradigms-joint-vs-end-effector-space&#34;&gt;Control Paradigms: Joint vs. End-Effector Space&lt;/h2&gt;&lt;p&gt;Following gym-aloha&amp;rsquo;s design, I implemented joint-space control where actions directly specify target joint positions:&lt;/p&gt;&lt;table&gt;  &lt;thead&gt;      &lt;tr&gt;          &lt;th&gt;Aspect&lt;/th&gt;          &lt;th&gt;Joint-space control&lt;/th&gt;      &lt;/tr&gt;  &lt;/thead&gt;  &lt;tbody&gt;      &lt;tr&gt;          &lt;td&gt;&lt;strong&gt;Action&lt;/strong&gt;&lt;/td&gt;          &lt;td&gt;Target joint positions → &lt;code&gt;data.ctrl&lt;/code&gt;&lt;/td&gt;      &lt;/tr&gt;      &lt;tr&gt;          &lt;td&gt;&lt;strong&gt;Control loop&lt;/strong&gt;&lt;/td&gt;          &lt;td&gt;Actuators drive joints toward commanded positions&lt;/td&gt;      &lt;/tr&gt;      &lt;tr&gt;          &lt;td&gt;&lt;strong&gt;Learning&lt;/strong&gt;&lt;/td&gt;          &lt;td&gt;Policy learns in robot&amp;rsquo;s natural DOF&lt;/td&gt;      &lt;/tr&gt;      &lt;tr&gt;          &lt;td&gt;&lt;strong&gt;Transfer&lt;/strong&gt;&lt;/td&gt;          &lt;td&gt;Direct mapping to real hardware&lt;/td&gt;      &lt;/tr&gt;  &lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;I chose joint-space over end-effector control for cleaner transfer to my target hardware. While end-effector control (where you specify gripper poses and let MuJoCo&amp;rsquo;s constraints solve for joint angles) can be more intuitive, it adds complexity that I wanted to avoid initially.&lt;/p&gt;&lt;h2 id=&#34;whats-inside-the-environment&#34;&gt;What&amp;rsquo;s Inside the Environment&lt;/h2&gt;&lt;p&gt;As of August 2025, gym-so100-c includes:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Simulation Environment &amp;amp; Tasks:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;MuJoCo-based SO101 simulation environment&lt;/li&gt;&lt;li&gt;Cube-to-bin task with configurable reward shaping&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Training Integration Scripts:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Reinforcement learning with Stable-Baselines3 (SAC)&lt;/li&gt;&lt;li&gt;Imitation learning with the &lt;code&gt;imitation&lt;/code&gt; library&lt;/li&gt;&lt;li&gt;Training integration with LeRobot (ACT, Diffusion, VLAs)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Control &amp;amp; Data Collection:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Teleoperation via keyboard or gamepad&lt;/li&gt;&lt;li&gt;Episode recording for demonstration datasets&lt;/li&gt;&lt;li&gt;Dataset conversion to LeRobot format&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;early-design-lessons&#34;&gt;Early Design Lessons&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Physics Engine Choice:&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;MuJoCo was the obvious choice for its speed, stability, and excellent contact modeling. It works well on CPU, it was perfect on MacBook.&lt;/p&gt;&lt;p&gt;I am excited about IsaacSIM, but I don&amp;rsquo;t have a powerful GPU yet. I am open to trying other engines in the future.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Observation Space Design:&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I experimented with different observation combinations:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;pixels_agent_pos&lt;/code&gt;: Camera images + joint positions&lt;/li&gt;&lt;li&gt;&lt;code&gt;agent_pos&lt;/code&gt;: Joint positions only (for faster training)&lt;/li&gt;&lt;li&gt;&lt;code&gt;pixels&lt;/code&gt;: Camera images only (for vision-based policies)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The mixed approach (&lt;code&gt;pixels_agent_pos&lt;/code&gt;) worked best, giving policies both rich visual information and precise proprioceptive feedback.This is also what ACT paper and most SOTA approaches are using.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Reward Engineering:&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;In Reinforce Learning an agent collects rewards interacting with the environment and tries to maximize them. The trick is to make such rewards that the agent does what you want from it and also that it can learn pretty well. Shaping the reward structure (designing the incentives) is called Reward Engineering and it&amp;rsquo;s suprisingly tricky to get right!&lt;/p&gt;&lt;p&gt;For example if you reward getting close to the block too much, the agent might chose to hover over it and never grab it. If you don&amp;rsquo;t give any rewards until the task is complete and if the task is too hard, there is no feedback to learn from.&lt;/p&gt;&lt;p&gt;I implemented both sparse rewards (success/failure only) and dense rewards with approach shaping. Dense rewards proved much more reliable for learning, though they required more careful tuning.&lt;/p&gt;&lt;p&gt;I also implememented HER (hindsight experience replay) that automates the reward shaping by trying to teach the robot behaviors from previous trajectories, but the results were mixed in my setup.&lt;/p&gt;&lt;h2 id=&#34;the-integration-challenge&#34;&gt;The Integration Challenge&lt;/h2&gt;&lt;p&gt;The real value of this environment isn&amp;rsquo;t just the simulation — it&amp;rsquo;s the integration layer that lets me seamlessly move between different learning approaches. In the next post, I&amp;rsquo;ll dive into what I learned from training experiments with SAC, behavior cloning, and modern imitation learning methods.&lt;/p&gt;&lt;p&gt;But the foundation was crucial: having a single environment that could work with multiple learning paradigms, generate consistent datasets, and provide reliable evaluation metrics. Sometimes the unglamorous infrastructure work is what makes everything else possible.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;Next up: Training experiments and what I learned about the practical differences between reinforcement learning and imitation learning approaches.&lt;/em&gt;&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;What&amp;rsquo;s your experience with simulation environments for robotics? Have you found certain design decisions that made experimentation much easier or harder? I&amp;rsquo;d love to hear about it.&lt;/em&gt;&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>My deep dive into robotics: Part 1</title>
       <link>https://tinystruggles.com/posts/robotics_journey_1/</link>
       <pubDate>Thu, 24 Jul 2025 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/robotics_journey_1/</guid>
       <description>&lt;p&gt;This post is the first in a series documenting my journey into robotics. I’m writing these to reflect on my progress and strategy, but also to help or inspire anyone considering a similar leap.&lt;/p&gt;&lt;p&gt;If you&amp;rsquo;re curious about AI, building physical systems, or carving out your own path into deep tech—this one’s for you.&lt;/p&gt;&lt;h2 id=&#34;why-robotics--my-background&#34;&gt;Why robotics &amp;amp; my background&lt;/h2&gt;&lt;p&gt;I grew up on a farm, so I’m no stranger to tedious manual labor. I also happen to be very lazy when it comes to physical work - as a kid I would never want to help out in the farm or doing chores. I&amp;rsquo;m not proud of it, but that&amp;rsquo;s true! I’d always rather be learning or building something than repeating the same task over and over.&lt;/p&gt;&lt;p&gt;Why not make robots do the boring work?&lt;/p&gt;&lt;p&gt;I’ve long believed that robotics—especially embodied intelligence—has huge potential to reshape critical parts of our world: infrastructure, food systems, care work. Europe, where I am based is facing an aging crisis and labor is getting more and more expensive and that drives the cost of housing, food and services. But seriously, I don&amp;rsquo;t think this trend needs to continue. Cheap electricity from atom/renewables + robotics can make labor cheap again and drastically improve quality of life for everyone if things go right (Yes, I&amp;rsquo;m a techno-optimist for sure!).&lt;/p&gt;&lt;p&gt;I’ve also been deeply into reinforcement learning for years. I remember doing the University of Alberta RL specialization on Coursera and being struck by the elegance of the math and the philosophical depth of the agent paradigm. It felt personal—figuring out how to learn, how to explore, how to act in an uncertain world. I could relate, as a human.&lt;/p&gt;&lt;p&gt;This project came at a natural inflection point. I had just finished a startup accelerator, moved back to Poland, gained access to a physical workshop with my brother, and met someone who introduced me to the Lerobot ecosystem.&lt;/p&gt;&lt;p&gt;During the startup accelerator, I had a lot of time to ponder what type of business I would like to do. Robotics was definitely something I was interested in, but I didn&amp;rsquo;t have a competitive edge, due to lack of practical experience.&lt;/p&gt;&lt;p&gt;I wanted to do something technically deep and fun—and potentially startup-worthy. Robotics fit perfectly.&lt;/p&gt;&lt;p&gt;Lerobot ecosystem really picked my interest and made actual robotics seem much more approachable and accessible. State of the art models on commodity, open hardware? Lead by Hugginface (a great brand name in the ML community)? Sign me up!&lt;/p&gt;&lt;h2 id=&#34;what-types-of-robotics-i-wanted-to-learn&#34;&gt;What types of robotics I wanted to learn&lt;/h2&gt;&lt;p&gt;I’m focused on manipulation—robot arms, gripping, interacting with the world. I’m less interested in mobile robots for now, and more excited by the intersection of learning-based control and practical use cases.&lt;/p&gt;&lt;p&gt;My application areas of interest were agriculture and construction. Both are high-impact and under-automated. Both also demand robustness and real-world intelligence—qualities modern AI might finally deliver.&lt;/p&gt;&lt;h2 id=&#34;timeline-and-the-plan&#34;&gt;Timeline and the plan&lt;/h2&gt;&lt;p&gt;I love learning and I&amp;rsquo;ve always loved to learn on my own. As an experienced self-learner, I’ve learned that investing upfront in a good learning plan pays off massively.&lt;/p&gt;&lt;p&gt;A few years back I read &lt;a href=&#34;https://www.goodreads.com/book/show/48803712-ultralearning&#34;&gt;Ultralearning by Scott Young&lt;/a&gt;, and one key takeaway stuck with me: people often spend too little time researching how to learn best, picking the resources and making a plan. If you are willing to spend hundreds of hours learning, pick wisely.&lt;/p&gt;&lt;p&gt;I am not a robotics expert, so figuring out a curriculum all by myself would be hard. I also have a pretty strong science/engineering/software background, so I didn&amp;rsquo;t want to follow something generic. So, what to do?&lt;/p&gt;&lt;p&gt;Well, good that we&amp;rsquo;ve got powerful LLMs to personalize our learning paths.&lt;/p&gt;&lt;p&gt;I used ChatGPT Deep Research to help me generate a 12-week roadmap. Using Deep Research was important, because it causes LLM to ask clarifying questions and scour the internet for you, saving potentially tens of hours of research and analysis.&lt;/p&gt;&lt;p&gt;The input: I had a background in software, ML and RL, was aiming for deep hands-on knowledge, had ~30 hours per week to spare, and wanted to explore robotics manipulation with an eye toward AgTech or construction startups.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;I already have some background in RL and ML. I want to learn about state of the art robotics to start a company. I don&amp;rsquo;t want to do a degree. I have time and money though. I am going to build and experiment with Lerobot from hugging face soon (waiting for last parts to arrive). I have access to 3d printers and heavy workshop. Create me a 3 month learning roadmap, with project ideas, assuming 30 hours of effort available per week.&lt;/p&gt;&lt;p&gt;I know python and I have many years of experience in software engineering.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;See the full chat history for the plan creation &lt;a href=&#34;https://chatgpt.com/g/g-p-681f759d59a88191834ddc95357bedb4-robotics-rl/c/681f7626-c5d4-8007-bd50-421672fc4b84&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The output was ambitious: a full-stack robotics journey mixing ROS2, LeRobot, reinforcement learning, imitation learning, simulation, hardware, and even startup-aligned thinking. It was crazy - but in a fun, exciting way.&lt;/p&gt;&lt;p&gt;I put the plan into google doc and I have shown it to some people. They didn&amp;rsquo;t have major concerns apart from &amp;ldquo;woah, really?&amp;rdquo; - but I&amp;rsquo;m used to reactions like this.&lt;/p&gt;&lt;p&gt;See the plan as an &lt;a href=&#34;https://docs.google.com/document/d/1Fkpylp1HfB8vHRUukHXmEH3RVO0KWSQDYcmz24lOYWU/edit?tab=t.0#heading=h.bijs78k1tfag&#34;&gt;exported Google Doc&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Why 12 weeks? This is part of my regular planning, I usually plan daily, weekly and quarterly. Twelve weeks is long enough to make real progress — but short enough to keep urgency high.&lt;/p&gt;&lt;h2 id=&#34;plan-vs-reality&#34;&gt;Plan vs Reality&lt;/h2&gt;&lt;p&gt;I created my plan on 16 May 2025. Now we have 25th July - we are 10 weeks in. Realistically, I focused maybe 7 weeks on robotics between other projects and travels.&lt;/p&gt;&lt;p&gt;So how did it go?&lt;/p&gt;&lt;p&gt;I started out following the roadmap pretty seriously. For the first two weeks, I was all in—spending ~30 hours a week, doing two full Udemy courses on ROS2 and manipulation, and diving deep into kinematics and motion planning. I even assembled the LeRobot SO101 arm around that time.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/lerobot_follower_hu_5d633f8dcc1fe4ea.jpg&#34;&gt;&lt;p&gt;But I never treated the plan as a rigid calendar. I thought of the “weeks” more like themed modules—open to remixing, repeating, or extending. I also gave myself permission to insert breaks between phases. It was aggressive by design, but flexible in execution.&lt;/p&gt;&lt;p&gt;Some things I deliberately skipped or reframed. For example, I didn’t do the vision pipeline in week 4—OpenCV felt outdated for where I saw the field going. I was more interested in learning-based approaches to perception. Similarly, while I didn’t use ROS2 with my SO101 arm, I did learn how to integrate ROS2 and Arduino in earlier work with a janky robot from the course. The skill was there—I just chose not to apply it in that form.&lt;/p&gt;&lt;p&gt;After two solid weeks, I reached a point where I felt I had enough foundational understanding to start building something real. That was the real pivot point: I asked myself, “What would it take to actually do a project?” That question reshaped my trajectory.&lt;/p&gt;&lt;p&gt;Instead of following the rest of the roadmap linearly, I shifted into execution mode. I was craving for a longer duration specific project - and I made another plan, this time a project focused one - Laundry Folding Robot inspired by aloha. &lt;a href=&#34;https://docs.google.com/document/d/1_VJKo8ugGwBziq_BhEmXFKISYCFgrnEnNNm5g0aPTCk/edit?tab=t.0#heading=h.t0rqoeeptztq&#34;&gt;See it here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;With my specific laundry folding robot in mind: I built a custom gym-style simulation matching the SO101 arm, and from there dove headfirst into reinforcement learning (mostly SAC), imitation learning (BC, GAIL, AIRL), diffusion policies, and VLA models like smolVLA and Pi0.&lt;/p&gt;&lt;p&gt;It didn&amp;rsquo;t match the tasks from the original plan — but at the same time, it was aligned with the curriculum I wanted to cover. The plan had 3 weeks fouced on RL, imitation and VLAs, I just did it from the point of a specific use case, instead of following the steps of the original plan.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/sim_hu_29968fbeb55a854a.png&#34;&gt;&lt;p&gt;Yup, and I managed to teach the robot how to throw the bin to the box.&lt;/p&gt;&lt;p&gt;(even though some of my teleoperation demonstrations were pretty poor - &lt;a href=&#34;https://huggingface.co/spaces/lerobot/visualize_dataset?path=%2Fattilczuk%2Fbin_sim3%2Fepisode_15&#34;&gt;see here&lt;/a&gt; - it&amp;rsquo;s not easy to control a robot!)&lt;/p&gt;&lt;p&gt;Reviewing the original plan: I didn’t touch sensing, SLAM, or domain-specific deployment yet. I already had some experiences with sensing (courses in the past, etc), but it&amp;rsquo;s definitely an area to dive deeper into, especially when I get into mobile robots.&lt;/p&gt;&lt;p&gt;The remaining: &amp;ldquo;Week 7: Advanced Sensing and Actuation – Toward Field Deployment&amp;rdquo;,  &amp;ldquo;Week 9: Sim-to-Real Transfer and Scaling Up&amp;rdquo;, &amp;ldquo;Week 10: Domain Focus – Agricultural and Construction Robotics Deep Dive&amp;rdquo; still seem very useful and interesting.&lt;/p&gt;&lt;p&gt;I did some data collections in real life, but my setup was super crappy:&lt;/p&gt;&lt;p&gt;(picture from before I invested in a tripod and more cameras)&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/so_teleop_hu_8ea7a33e66893112.jpg&#34;&gt;&lt;p&gt;The last week of the plan was meant to be spent on reflection and I&amp;rsquo;m doing it ahead of schedule since the execution has gone off plan, while still on track for the high level goals.&lt;/p&gt;&lt;h2 id=&#34;talking-to-people&#34;&gt;Talking to people&lt;/h2&gt;&lt;p&gt;My official learning plan didn’t include speaking with others. But I’ve come to appreciate just how much easier — and more energizing — hard things become with the right people around.&lt;/p&gt;&lt;p&gt;I visited two robotics labs in Warsaw and spoke with researchers there. One lab focused on swarm robotics and was led by someone with a clear entrepreneurial mindset. It was inspiring to see someone thinking about scalable systems and real-world impact. The other lab worked on human-robot interaction, especially with children — a domain I hadn’t seriously considered before, but one that sparked new curiosity.&lt;/p&gt;&lt;p&gt;Both labs invited me to collaborate and offered access to their robots. That kind of openness was encouraging. While neither was working directly with deep learning for robotics — the area I&amp;rsquo;m most drawn to — these conversations broadened my view of what robotics can be. They gave me strength and a sense of possibility. At one of the labs, I even reconnected with an old friend I hadn’t seen in over a decade. It was unexpectedly meaningful to find someone I knew already immersed in this space.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/lab_2_hu_94f79c88c0e4eafe.jpg&#34;&gt;&lt;p&gt;I also reached out to people at Warsaw-based robotics companies like nomagic.ai. So far, those connections haven’t led anywhere, but I’ll keep exploring. I missed the LeRobot worldwide hackathon, but joined the Discord and have been in occasional contact with the friend who introduced me to the platform.&lt;/p&gt;&lt;p&gt;I started this journey solo — with a roadmap, tools, and focus. But if I do pursue something deeptech, I know I’ll want collaborators. These early conversations reminded me that the right people can shift what feels possible.&lt;/p&gt;&lt;h2 id=&#34;reflection-on-the-learning&#34;&gt;Reflection on the learning&lt;/h2&gt;&lt;p&gt;The roadmap was wildly ambitious. Some topics were PhD-level, and there’s no way to do justice to them all in three months. Having a tight schedule was helpful to prevent me from going to deep on any specific topic, it would be fun, but I didn&amp;rsquo;t want to lose the sight of the bigger picture.&lt;/p&gt;&lt;p&gt;Having a plan really helped, even if I didn&amp;rsquo;t stick to it strictly. It helped me to have a somewhat structured personalized learning plan.&lt;/p&gt;&lt;p&gt;The biggest takeaway: mixing structured learning with project-driven deep dives is incredibly effective. Courses gave me confidence; projects gave me momentum.&lt;/p&gt;&lt;p&gt;Some parts of the roadmap (like OpenCV pipelines or QA protocols) felt less relevant with how fast deep learning is progressing—but that’s fine. It’s all about strategic depth, not completeness.&lt;/p&gt;&lt;h2 id=&#34;reflection-on-the-plan&#34;&gt;Reflection on the plan&lt;/h2&gt;&lt;p&gt;The 12-week structure gave me an early push, but I was always open to adjusting it. I didn’t treat the roadmap as a rigid calendar—it was more like a menu.&lt;/p&gt;&lt;p&gt;In retrospect, I think mixing theory and practice is hard to pre-plan. That’s where I deviated the most. But having the map made it easier to navigate.&lt;/p&gt;&lt;h3 id=&#34;what-else-from-the-plan-do-i-plan-to-do&#34;&gt;What else from the plan do I plan to do?&lt;/h3&gt;&lt;p&gt;I still want to:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Build a basic vision pipeline + scripted action demo.&lt;/li&gt;&lt;li&gt;Explore field deployment topics (SLAM, navigation).&lt;/li&gt;&lt;li&gt;Learn more about sim-to-real strategies.&lt;/li&gt;&lt;li&gt;Do a more integrated prototype + test cycle for a specific use case (e.g. fold my laundry!)&lt;/li&gt;&lt;li&gt;Revisit AgTech and construction applications with more realism. Or pivot to another industry entirely (e.g. care sector).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Additionally I plan to:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Make better demonstrations&lt;/li&gt;&lt;li&gt;More good demos with real robots&lt;/li&gt;&lt;li&gt;Build more robots - LeKiwi and &lt;a href=&#34;https://github.com/Vector-Wangel/XLeRobot/tree/main&#34;&gt;Xlerobot&lt;/a&gt; are next!&lt;/li&gt;&lt;li&gt;Instead of e2e VLAs also try out LLM + IK over MCP&lt;/li&gt;&lt;li&gt;Invest in better simulation - either IsaacSIM or Maniskill&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;using-ai-for-learning--projects&#34;&gt;Using AI for learning / projects&lt;/h2&gt;&lt;p&gt;I used AI extensively during this learning journey:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;it helped me crafting my plan and find the right resources to learn from&lt;/li&gt;&lt;li&gt;explaining specific concepts&lt;/li&gt;&lt;li&gt;drafting anki cards so I can remember the concepts better&lt;/li&gt;&lt;li&gt;help troubleshooting installations&lt;/li&gt;&lt;li&gt;help coding and ML training&lt;/li&gt;&lt;li&gt;emotional support during RL training (seriously, it was needed, many models are prone to diverging) together with hyperparameter tuning&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I used chatGPT, Claude, Gemini and VSCode with Copilot (powered mostly Claude Sonnet). I briefly tried gemini CLI, but this tool has much more to offer. I just scratched the surface.&lt;/p&gt;&lt;p&gt;I created a Project within chatGPT where I included my plans as project files and that helped a lot to focus the AI.&lt;/p&gt;&lt;p&gt;My project instructions:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;You are a robotics/RL expert and an expert educator. You are my learning assistant. Think more coach than a teacher.&lt;/p&gt;&lt;p&gt;Be concise and do not come up with suggestions without understanding what is needed from you. Ask a lot of questions before suggesting anything or sharing an opinion. I want to think independently, help me with that. Any time you offer a suggestion, make sure to justify why it&amp;rsquo;s needed and why it&amp;rsquo;s better than alternative approaches.&lt;/p&gt;&lt;/blockquote&gt;&lt;h2 id=&#34;resources-i-recommend&#34;&gt;Resources I recommend&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&#34;https://github.com/huggingface/lerobot&#34;&gt;Lerobot&lt;/a&gt; - open hardware, tutorials, easy to access models and datasets - maybe a bit confusing and unstable at times, but amazing! Additionally they have a lively community!&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://www.coursera.org/specializations/reinforcement-learning&#34;&gt;Reinforcement Learning Course&lt;/a&gt; - very approachable&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://rail.eecs.berkeley.edu/deeprlcourse/&#34;&gt;CS 285 Deep RL Course&lt;/a&gt; - not for the faint of heart, lots of SOTA techniques&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://hades.mech.northwestern.edu/index.php/Modern_Robotics&#34;&gt;Modern Robotics Course&lt;/a&gt; - I would call it classic robotics in the context of my learning, but it covers a lot of useful fundamentals&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://www.udemy.com/course/ros2-for-beginners&#34;&gt;ROS2 Basics Course&lt;/a&gt; - was useful for understanding ROS2, URDF (how robots are described in software), Gazebo for simulation&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://www.udemy.com/course/robotics-and-ros-2-learn-by-doing-manipulators/?couponCode=MT240725G1&#34;&gt;ROS2 Manipulators Course&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;conclusions--tips&#34;&gt;Conclusions &amp;amp; tips&lt;/h2&gt;&lt;p&gt;If you’re aiming for a deep-tech startup, playing the long game on learning is worth it.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;You need a mix of practical experience and theory to back it up&lt;/li&gt;&lt;li&gt;Structured courses are the best way to acquire theoretical knowledge&lt;/li&gt;&lt;li&gt;Practical projects are the best to keep motivated while learning a plethora of tools and approaches&lt;/li&gt;&lt;li&gt;Plans are tools, not rules. Break them if you need to.&lt;/li&gt;&lt;li&gt;Having a clear project goal helped me stay focused and curious.&lt;/li&gt;&lt;li&gt;Being overwhelmed is normal. Keep going anyway. Use friends and Claude for emotional support.&lt;/li&gt;&lt;li&gt;Buying in literally by spending money on hardware made it much more real&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Next up: in Part 2, I’ll dive into my current setup, I will share my simulation env and scripts I used to integrate with:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;mujoco / gym&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://stable-baselines.readthedocs.io/en/master/index.html&#34;&gt;stablebaselines3&lt;/a&gt; (RL)&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://imitation.readthedocs.io/en/latest/&#34;&gt;imitation&lt;/a&gt; (IL)&lt;/li&gt;&lt;li&gt;lerobot (Diffusion, ACT, VLAs)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;including training, teleoperation, storing/uploading datasets in multiple formats and eval. I&amp;rsquo;m happy to share my code in help that it helps people with integrations as these tools can be overwhelming.&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Generating Hundreds of Consistent Illustrations with Gemini Image Generation</title>
       <link>https://tinystruggles.com/posts/illustrations_with_gemini/</link>
       <pubDate>Sun, 20 Jul 2025 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/illustrations_with_gemini/</guid>
       <description>&lt;p&gt;&lt;em&gt;A deep dive into building an automated illustration pipeline for storytelling applications&lt;/em&gt;&lt;/p&gt;&lt;h2 id=&#34;introduction&#34;&gt;Introduction&lt;/h2&gt;&lt;p&gt;AI image generation has revolutionized creative workflows, but there&amp;rsquo;s a significant difference between generating a single stunning image and producing hundreds of consistent illustrations for a complete project. When building &lt;a href=&#34;https://storylearner.app&#34;&gt;storylearner.app&lt;/a&gt;, we faced the challenge of generating book illustrations that maintained visual consistency while telling compelling stories through imagery.&lt;/p&gt;&lt;p&gt;We&amp;rsquo;ve used powerful Gemini multimodal models, mostly because they are fast and because the experimental ones are available for free.&lt;/p&gt;&lt;p&gt;This article explores the technical and creative challenges of large-scale AI illustration generation, showcasing techniques for achieving visual consistency and building robust pipelines that can handle the complexity of full book illustration projects.&lt;/p&gt;&lt;p&gt;Below are images created for a chapter of a book adapted for the storylearner platform:&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/illustrations_storylearner_hu_b556b7f21473fa90.png&#34;&gt;&lt;p&gt;The article has a companion &lt;a href=&#34;https://colab.research.google.com/drive/1BTDh8RQRfK2ghS2NfK5LOwoGeR2WEYdR#scrollTo=EjB4lHG1WgId&#34;&gt;colab notebook&lt;/a&gt;, so you can play with the examples yourself.&lt;/p&gt;&lt;h2 id=&#34;the-consistency-challenge&#34;&gt;The Consistency Challenge&lt;/h2&gt;&lt;h3 id=&#34;understanding-visual-consistency-in-ai&#34;&gt;Understanding Visual Consistency in AI&lt;/h3&gt;&lt;p&gt;AI image generation models operate somewhat like a company of talented artists, each with amnesia. Every generation is essentially a fresh start unless you provide explicit context. This creates unique challenges when you need to maintain character consistency, style coherence, and narrative flow across hundreds of images.&lt;/p&gt;&lt;p&gt;Consider this simple experiment: generating three images of &amp;ldquo;the same pig with wings and a top hat flying over a futuristic city&amp;rdquo; in different weather conditions. Even with identical prompts and seeds, subtle inconsistencies emerge—eye colors change, proportions shift, and the overall character can feel different.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/pigs_ok_hu_f92d60e8f0ad9ab4.png&#34;&gt;&lt;h3 id=&#34;the-impact-of-seeds-and-prompts&#34;&gt;The Impact of Seeds and Prompts&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Seeds&lt;/strong&gt; act like selecting a specific artist from your AI company. Using the same seed with identical prompts yields consistent results, but even small prompt variations can dramatically alter the output. We discovered that:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Same model + same prompt + same seed&lt;/strong&gt; = identical results&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Same model + same prompt + different seed&lt;/strong&gt; = completely different character&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Same model + slightly altered prompt + same seed&lt;/strong&gt; = often produces a different character entirely&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This sensitivity means that scaling up requires careful orchestration of all these variables.&lt;/p&gt;&lt;h2 id=&#34;building-consistency-through-reference-images&#34;&gt;Building Consistency Through Reference Images&lt;/h2&gt;&lt;h3 id=&#34;the-reference-image-approach&#34;&gt;The Reference Image Approach&lt;/h3&gt;&lt;p&gt;The most reliable method we found for maintaining character consistency involves using reference images. Here&amp;rsquo;s how it works:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Generate an initial character/scene&lt;/strong&gt; using carefully crafted prompts&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Upload this image&lt;/strong&gt; as a reference for subsequent generations&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Include explicit instructions&lt;/strong&gt; like &amp;ldquo;Use the supplied image as a reference for how the pig should look like&amp;rdquo;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;This approach significantly improves consistency, though it can also cause new issues in some very specific cases.&lt;/p&gt;&lt;p&gt;With the following reference image:&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/pig_reference_hu_f98f113755723c2f.png&#34;&gt;&lt;p&gt;You can get this different, but consistent one!&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/pig_ok_hu_58d8da051942c57a.png&#34;&gt;&lt;p&gt;Here is an example of triggering a specific case:&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/pigs_bad_hu_fd4367d427b811a5.png&#34;&gt;&lt;p&gt;This specific case can happen when a prompt is pretty similar to the one that generated the initial image and the seed is identical.So, when using the reference image and similar prompt, I would actually recommend to change the seed or not set it.&lt;/p&gt;&lt;p&gt;See the reference &lt;a href=&#34;https://colab.research.google.com/drive/1BTDh8RQRfK2ghS2NfK5LOwoGeR2WEYdR#scrollTo=Dw9e0U_gNjPK&#34;&gt;colab&lt;/a&gt; for details.&lt;/p&gt;&lt;h3 id=&#34;practical-implementation&#34;&gt;Practical Implementation&lt;/h3&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Upload reference image&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;files &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [client&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;files&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;upload(file&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;reference_character.png&amp;#34;&lt;/span&gt;)]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Create parts with reference and prompt&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;parts &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    types&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Part&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;from_uri(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        file_uri&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;files[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;]&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;uri,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        mime_type&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;files[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;]&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;mime_type,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    types&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Part&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;from_text(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        text&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;prompt &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;Use the supplied image as a reference for character appearance&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Generate with reference&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;response &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; client&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;generate_content(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    model&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;IMAGE_MODEL,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    contents&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;parts,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    config&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;types&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;GenerateContentConfig(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        response_modalities&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;Text&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;Image&amp;#39;&lt;/span&gt;],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# No seed set.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-storylearnerapp-illustration-pipeline&#34;&gt;The Storylearner.app Illustration Pipeline&lt;/h2&gt;&lt;h3 id=&#34;high-level-architecture&#34;&gt;High-Level Architecture&lt;/h3&gt;&lt;p&gt;Our production pipeline consists of three main stages:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Idea Generation&lt;/strong&gt;: Story text + guidelines → 3 illustration concepts per scene&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Idea Selection&lt;/strong&gt;: Multiple concepts → best ideas chosen for the complete set&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Image Generation&lt;/strong&gt;: Selected ideas + style guidelines + reference images → final illustrations&lt;/li&gt;&lt;/ol&gt;&lt;h3 id=&#34;stage-1-brainstorming-illustration-ideas&#34;&gt;Stage 1: Brainstorming Illustration Ideas&lt;/h3&gt;&lt;p&gt;Rather than feeding story text directly to image generation (which often produces poor results), we separate conceptualization from execution:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;IdeaGenerator&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;generate_illustration_ideas&lt;/span&gt;(self, text: str, context: str):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        prompt &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        You are a visual scene designer. Based on the story below, &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        describe 3 different highly detailed and imaginative illustration ideas. &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        Do not include any people or humanoid figures. Focus on setting, &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        atmosphere, lighting, symbolic objects, and environmental storytelling.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        Story: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{text}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        Context: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{context}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# Returns 3 detailed scene descriptions per text excerpt&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This approach generates rich, detailed scene descriptions that serve as blueprints for image generation.&lt;/p&gt;&lt;h3 id=&#34;stage-2-intelligent-selection&#34;&gt;Stage 2: Intelligent Selection&lt;/h3&gt;&lt;p&gt;To avoid repetitive illustrations (like &amp;ldquo;a ship, a ship, a ship&amp;rdquo; in a sea voyage story), we use an AI selector to choose the best combination of ideas:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;IllustrationSelector&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;select_illustrations&lt;/span&gt;(self, chapter):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        prompt &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        Choose the best idea for each illustration considering that:&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        - The set should be diverse&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        - Illustrations shouldn&amp;#39;t contain people&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        - Prefer illustrations matching the provided titles&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# Returns optimal selection indices&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;stage-3-consistent-generation&#34;&gt;Stage 3: Consistent Generation&lt;/h3&gt;&lt;p&gt;The final generation stage uses:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Style guidelines&lt;/strong&gt; (detailed visual specifications)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Reference images&lt;/strong&gt; for style consistency&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Persistent chat sessions&lt;/strong&gt; for maintaining context&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Retry mechanisms&lt;/strong&gt; for handling API limitations&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;visual-guidelines-and-style-consistency&#34;&gt;Visual Guidelines and Style Consistency&lt;/h2&gt;&lt;h3 id=&#34;crafting-effective-style-guidelines&#34;&gt;Crafting Effective Style Guidelines&lt;/h3&gt;&lt;p&gt;We developed comprehensive style guidelines that go beyond simple style names:&lt;/p&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Style: watercolorTechnique: Combine soft watercolor washes with fine ink line work for contrast and detail.Brushwork: Embrace visible brush strokes, blooming, and natural texture.Ink Lines: Use varied line weights for depth; apply cross-hatching or stippling for texture.Color Palette: Limit to a few harmonious hues with gentle gradations.Forms: Use simplified, geometric shapes; focus on essence over detail.White Space: Treat negative space as part of the composition.Texture: Highlight watercolor paper&amp;#39;s natural texture and color variation.Atmosphere: Create light, airy scenes with openness and subtle contrast.Aesthetic: Preserve a hand-drawn look—embrace imperfections and human touch.&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;chat-based-generation-for-context-continuity&#34;&gt;Chat-Based Generation for Context Continuity&lt;/h3&gt;&lt;p&gt;Using persistent chat sessions helps maintain consistency within illustration sets:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;generate_set_of_illustrations&lt;/span&gt;(ideas_with_file_paths, pass_image&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    chat &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; client&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;chats&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;create(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        model&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;IMAGE_MODEL,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        config&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;types&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;GenerateContentConfig(response_modalities&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Text&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Image&amp;#34;&lt;/span&gt;]),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# Initialize with style guidelines and reference image&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    initial_prompt &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    You are a creative artist helping on an illustration project.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    Create &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;len(ideas_with_file_paths)&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt; beautiful illustrations.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    VISUAL GUIDELINES:&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;VISUAL_GUIDELINES&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# Generate each illustration within the same chat context&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; idea &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; ideas_with_file_paths:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        response &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; chat&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;send_message(format_illustration_prompt(idea))&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# Process and save generated image&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;avoiding-common-pitfalls&#34;&gt;Avoiding Common Pitfalls&lt;/h2&gt;&lt;h3 id=&#34;critical-design-decisions&#34;&gt;Critical Design Decisions&lt;/h3&gt;&lt;p&gt;Through extensive experimentation, we identified several key strategies:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Avoid Human Close-ups&lt;/strong&gt;: Character face consistency is extremely challenging. Focus on environmental storytelling instead.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;No Violence or Gore&lt;/strong&gt;: Keep illustrations family-friendly and avoid content that might trigger safety filters.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Diversify Scene Types&lt;/strong&gt;: The selection stage prevents repetitive imagery across the complete set.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Decouple Ideation from Generation&lt;/strong&gt;: Separating concept creation from image generation improves both quality and debuggability.&lt;/p&gt;&lt;h2 id=&#34;real-world-example-illustrating-the-three-musketeers&#34;&gt;Real-World Example: Illustrating The Three Musketeers&lt;/h2&gt;&lt;p&gt;Let&amp;rsquo;s walk through illustrating a chapter from &lt;em&gt;The Three Musketeers&lt;/em&gt;:&lt;/p&gt;&lt;h3 id=&#34;context-and-settings&#34;&gt;Context and Settings&lt;/h3&gt;&lt;p&gt;First, we establish the story context:&lt;/p&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Setting: France, primarily Meung and Paris, early 17th centuryHistorical Context: Political tensions between French monarchy and Cardinal RichelieuMain Characters: D&amp;#39;Artagnan, Athos, Porthos, Aramis, Cardinal Richelieu, Milady de Winter&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;generated-ideas&#34;&gt;Generated Ideas&lt;/h3&gt;&lt;p&gt;For the chapter opening, our system generated these concepts:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;&amp;ldquo;The Jolly Miller Inn Chaos&amp;rdquo;&lt;/strong&gt;: Exterior scene with a yellow pony, scattered debris, and dramatic lighting hinting at recent altercation&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&amp;ldquo;Broken Sword&amp;rdquo;&lt;/strong&gt;: Close-up of shattered steel on cobblestones, symbolizing lost honor and broken dreams&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&amp;ldquo;Inn Kitchen Aftermath&amp;rdquo;&lt;/strong&gt;: Dimly lit interior with earthenware, bandages, and flickering candlelight&lt;/li&gt;&lt;/ol&gt;&lt;h3 id=&#34;selection-and-generation&#34;&gt;Selection and Generation&lt;/h3&gt;&lt;p&gt;The selector chose the most diverse and narratively appropriate ideas, which were then generated using our reference image and style guidelines, producing illustrations that maintain visual consistency while telling the story effectively.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/musketeers_3_hu_672d771d5adf2ab5.png&#34;&gt;&lt;h2 id=&#34;key-insights-and-best-practices&#34;&gt;Key Insights and Best Practices&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Consistency vs. Perfection&lt;/strong&gt;: Perfect consistency isn&amp;rsquo;t always necessary—visual coherence in style and mood often matters more than exact character matching.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Artist Analogy&lt;/strong&gt;: Think of AI models as artists with amnesia. You need to provide context, references, and clear instructions for each interaction.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pipeline Modularization&lt;/strong&gt;: Breaking the process into idea generation, selection, and execution improves quality and maintainability.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Style Guidelines Matter&lt;/strong&gt;: Detailed, specific style descriptions work better than simple style names.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reference Images Are Crucial&lt;/strong&gt;: Upload and reference style examples for best consistency results.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Long Sessions are Fragile&lt;/strong&gt;: It often works until a point, and at some point it fails poorly, e.g. inserting objects from a previous illustration into the following ones.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;There is still a huge gap between a carefully handcrafted demo on an AI company blog and practical usage of the technology at scale. Things don&amp;rsquo;t work out well straight out of the box, but you can make these amazing tools work for you with help of systematic thinking about consistency, quality control, and robust engineering practices.&lt;/p&gt;&lt;p&gt;While challenges remain, I hope you&amp;rsquo;ll enjoy the techniques we&amp;rsquo;ve developed at &lt;a href=&#34;https://storylearner.app&#34;&gt;storylearner.app&lt;/a&gt; to power your own projects!&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Escaping LLM piping mess with nifty engineering</title>
       <link>https://tinystruggles.com/posts/messy_colab_to_content_studio/</link>
       <pubDate>Wed, 25 Jun 2025 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/messy_colab_to_content_studio/</guid>
       <description>&lt;p&gt;In this post I’ll walk through how I upgraded a set of tangled Python notebooks—responsible for thousands of LLM calls—into a robust content-adaptation studio powered by:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;an async FastAPI pipeline,&lt;/li&gt;&lt;li&gt;a disk-first Next.js frontend, and&lt;/li&gt;&lt;li&gt;a small suite of custom CLI tools.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Re-engineering the stack was essential for my own sanity: the notebooks were fragile, slow to iterate on, and far too labor-intensive to babysit.&lt;/p&gt;&lt;p&gt;I was also facing content quality challenges that were pretty much impossible to address in the old code base, that re-engineering unlocked.&lt;/p&gt;&lt;p&gt;My hope is that the story also &lt;strong&gt;nudges you to build (or level-up) your own tooling&lt;/strong&gt; instead of settling for one-off notebooks.&lt;/p&gt;&lt;p&gt;We’ll cover:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Engineering constraints&lt;/strong&gt; – huge text volumes, strict meaning preservation, multiple target languages.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;The original notebook setup&lt;/strong&gt; – what worked and where it hurt.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Pain points&lt;/strong&gt; – why small hacks no longer cut it.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;The new architecture&lt;/strong&gt; – key design choices, novel elements (with screenshots), and how they solve the earlier pain.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Outcomes &amp;amp; takeaways&lt;/strong&gt; – higher quality, less toil, faster experiments, and patterns you can reuse in your own LLM workflows.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&#34;shape-of-the-problem&#34;&gt;Shape of the problem&lt;/h2&gt;&lt;p&gt;Shape of the problem:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;large quantities of text that need to be processed in a very specific way:&lt;ul&gt;&lt;li&gt;retain meaning of the original (no text disappearing or altered significantly)&lt;/li&gt;&lt;li&gt;consistent between parts&lt;/li&gt;&lt;li&gt;translated &amp;amp; simplified&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;resulting content needs to consistently have good quality&lt;ul&gt;&lt;li&gt;automatic quality assessment and quality repair (revisions)&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;A similar challenge would be relevant in translating legal documents, healthcare documents, etc.&lt;/p&gt;&lt;h2 id=&#34;story-learner-book-adaptation-needs&#34;&gt;Story Learner Book Adaptation needs&lt;/h2&gt;&lt;p&gt;In my project &lt;a href=&#34;https://storylearner.app&#34;&gt;StoryLearner&lt;/a&gt; I offer adapted books for language learning at a specific level. For example, &lt;a href=&#34;https://www.storylearner.app/book/dec14f38-dfc7-47ca-a839-d70a64cf19df&#34;&gt;&amp;ldquo;Las Aventuras de Sherlock Holmes, in A2, Spanish&amp;rdquo;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;We use LLMs for both language/level adaptation as well as for illustrations.It&amp;rsquo;s a lot of LLM calls (easily thousands for a single book), because books are long and there are many elements for a single adaptation.&lt;/p&gt;&lt;p&gt;A book has chapters, chapters have parts for easier reading. Each book/chapter/page has an custom illustration. Additionally, there are titles and descriptions to be adapted and transcribed.&lt;/p&gt;&lt;p&gt;Books are long and we want the adapted text to retain the meaning, while making the language simple (aligned with the target level), natural sounding and correct.&lt;/p&gt;&lt;h2 id=&#34;trouble-with-a-flaky-slow-pipeline-and-hard-to-assess-output&#34;&gt;Trouble with a flaky, slow pipeline and hard to assess output&lt;/h2&gt;&lt;p&gt;The pipeline was was quite a feat! It was a lot of LLM calls, built mostly in colab/jupyter notebooks.&lt;/p&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;RAW BOOK   |   v[book_stripping]   |   v[chapter_extraction]   |   v[chapter_simplification] (English)   |   v[chapter_partification] (English)   |                              \   |                               \    |                                ---&amp;gt; [illustration_generation]   |   v[adapt] ──▶ [Lang 1]   │        [Lang 2]   │        [Lang 3]   │        [Lang 4]   │        [Lang 5]   │        [Lang 6]   |&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Illustration generation was on its own a pretty interesting pipeline (more about it in a separate post!).&lt;/p&gt;&lt;p&gt;Here is one of the adaptation notebooks:&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/colab_hu_1b82808eed260bed.png&#34;&gt;&lt;p&gt;As you can see, it has its own table of contents on the side. It&amp;rsquo;s easily thousands of lines of code and prompts. And the hundreds of outputs (text and images) could make it very, very, long. To the point that it would have rendering issues.&lt;/p&gt;&lt;p&gt;On top of it there was:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Separate notebook for &lt;code&gt;book_narration&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Another notebook for upload to storylearner (via API).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The &amp;ldquo;pipeline&amp;rdquo; worked. It serialized partial outputs in a way that partial redos/continuations were possible, it offered decent visualization. It was adjustable (just add/tweak a notebook cell!).&lt;/p&gt;&lt;p&gt;However it was fragile and assessing quality/redoing content was painful and slow. And it was very frustrating to me, especially since the quality was important to my partners (language schools).&lt;/p&gt;&lt;h3 id=&#34;main-pain-points&#34;&gt;Main pain points&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;LLM reliability issues (no resources, surprise safety controls kicking in, running out of quota or LLMs not following instructions) broke downstream steps.&lt;/li&gt;&lt;li&gt;The adaptation process was slow, it wasn&amp;rsquo;t taking advantage of paralellization well&lt;/li&gt;&lt;li&gt;The pipeline was already so brittle enough that meaningful experiments were nearly impossible&lt;/li&gt;&lt;li&gt;Colab/notebooks encouraged slapping things together instead of proper engineering with encapsulation and tests&lt;/li&gt;&lt;li&gt;Python notebooks having rendering bugs because they were so long and had so many outputs (large text/many images)&lt;/li&gt;&lt;li&gt;Low confidence in language &lt;strong&gt;level&lt;/strong&gt;, name/format &lt;strong&gt;consistency&lt;/strong&gt;, and preserved &lt;strong&gt;meaning&lt;/strong&gt; without having a strict review/repair process and having some examples of problems with quality.&lt;/li&gt;&lt;li&gt;Reviewing 60+ chapters across six languages was slow and manual - was infeasible for me.&lt;/li&gt;&lt;li&gt;A copy of an adaptation notebook per book (for visualization/auditability) was duplicating the code and making it harder to maintain&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;new-content-adaptation-tools&#34;&gt;New Content Adaptation Tools&lt;/h2&gt;&lt;p&gt;I haven&amp;rsquo;t built everything at once. I started with a frontend using the existing disk format of book adaptations, then as it was easier to see what was going on, I progressively built more and more backend migrating specific functionalities. Fast API gives a nice UI out of the bat to call APIs, which was nice for trying things out. But the workflows were simply to long to drive them by hand, so that is how CLI tools came to be.&lt;/p&gt;&lt;p&gt;CLIs/Frontend were in big part written by copilot coding agent. I also extensively discussed the component prompts with LLMs 😀.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Backend – &lt;code&gt;bookadaptation&lt;/code&gt;&lt;/strong&gt; (FastAPI)&lt;/p&gt;&lt;ul&gt;&lt;li&gt;all functionality behind endpoints, all async / internally parallel where safe (28 separate endpoints)&lt;/li&gt;&lt;li&gt;All IO and LLM calls done async&lt;/li&gt;&lt;li&gt;Pipeline stages are classes; 11 &lt;code&gt;adapter&lt;/code&gt; subclasses (Gemini Flash, Gemini Pro, GPT-4o, etc.).&lt;/li&gt;&lt;li&gt;&lt;code&gt;skip&lt;/code&gt; and &lt;code&gt;use_cached&lt;/code&gt; flags run a no-op or reuse artefacts while the file tree stays unchanged.&lt;/li&gt;&lt;li&gt;&lt;code&gt;STAGE_DEPENDENCIES&lt;/code&gt; mapping declares primary &amp;amp; secondary inputs and outputs for every stage.&lt;/li&gt;&lt;li&gt;Hierarchical on-disk structure; files get a &lt;code&gt;_{revision_number}&lt;/code&gt; suffix for multi-round outputs.&lt;/li&gt;&lt;li&gt;Heavy use of controlled generation (using schemas)&lt;/li&gt;&lt;li&gt;SQL instrumentation: All prompts, schemas, settings, outputs, latency, and errors logged to SQLite.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Frontend – &lt;code&gt;ContentTools&lt;/code&gt; (Next.js Server Components)&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Specialised views:&lt;ul&gt;&lt;li&gt;Book language adaptations&lt;ul&gt;&lt;li&gt;Illustrate all artefacts of the adaptation pipeline (inputs, outpus)&lt;/li&gt;&lt;li&gt;Easy debugging of what happened during the QA&lt;/li&gt;&lt;li&gt;Easy comparisons between experimental implementations&lt;/li&gt;&lt;li&gt;“Final for publish” view.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;Illustrations:&lt;ul&gt;&lt;li&gt;All book illustrations as grids (chapters, chapter parts)&lt;/li&gt;&lt;li&gt;Illustration deep dives - ideas and the best ideas&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;Reads JSON &amp;amp; WebP directly from disk—no extra HTTP hop.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;CLI tools (all async friendly python)&lt;/p&gt;&lt;ul&gt;&lt;li&gt;adaptation CLI&lt;ul&gt;&lt;li&gt;Adapts and revises every chapter until it&amp;rsquo;s good enough&lt;/li&gt;&lt;li&gt;Drives the pipeline stages implemented in the service (many endpoints right)&lt;/li&gt;&lt;li&gt;Gracefully retries&lt;/li&gt;&lt;li&gt;LLM driven decisions: Uses output from the overall review to finish the chapter or go for more rounds or partially skip stages&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;narration CLI&lt;/li&gt;&lt;li&gt;illustration CLI&lt;/li&gt;&lt;li&gt;publishing CLI&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Everything runs locally, but could as well run on a server.&lt;/p&gt;&lt;p&gt;Yes, I ended up building a lightweight custom pipeline orchestration&amp;hellip; 💀&lt;/p&gt;&lt;h3 id=&#34;flow-of-a-book-adaptation-with-automatic-qa-multiple-revisions&#34;&gt;Flow of a book adaptation with automatic QA (multiple revisions)&lt;/h3&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;RAW BOOK   |   v[book_stripping]   |   v[chapter_extraction]   |   v[chapter_simplification] (English)   |   v[chapter_partification] (English)   |                              \   |                               \    |                                ---&amp;gt; [illustration_generation]   |   v[adapt] ──▶ [Lang 1]   │        [Lang 2]   │        [Lang 3]   │        [Lang 4]   │        [Lang 5]   │        [Lang 6]   |   v[review_chapter]  ←──────────────┐   |                             │   v                             │[revise_chapter]                │   |                             │   v                             │[review_consistency]            │   |                             │   v                             │[revise_consistency]            │   |                             │   v                             │[review_meaning_cohesion]       │   |                             │   v                             │[revise_meaning_cohesion]       │   |                             │   v                             │[review_titles]                 │   |                             │   v                             │[revise_titles]                 │   |                             │   v                             │[review_chapter_title]          │   |                             │   v                             │[revise_chapter_title]          │   |                             │   v                             │[review_overall]  ──────────────┘   |   v                             [promote_content]   |   |   +--&amp;gt; [book_narration] (from adapted text)   |   +--&amp;gt; [upload / publish]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;QA rounds are controlled by the output of the &lt;code&gt;review_overall&lt;/code&gt; stage.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/review_hu_cecba602a81f518c.png&#34;&gt;&lt;h3 id=&#34;examples-of-frontend-enabling-fast-qa&#34;&gt;Examples of frontend enabling fast QA&lt;/h3&gt;&lt;p&gt;Example of problematic images that can be &amp;rsquo;easily spotted&amp;rsquo; by a trained eye.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/image_grid_hu_c20e951bd58fd2f8.png&#34;&gt;&lt;p&gt;Chapter level issues overview for an adaptation workflow:&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/chapter_level_hu_a2bc8bd78ae124c4.png&#34;&gt;&lt;p&gt;Part level debugging/review of what was suggested/applied in the QA pipeline:&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/part_level_hu_3897d3eb68561cab.png&#34;&gt;&lt;h3 id=&#34;review-and-revisionkept-deliberately-apart&#34;&gt;Review and Revision—kept deliberately apart&lt;/h3&gt;&lt;p&gt;One key design choice was to &lt;strong&gt;decouple &amp;ldquo;reviewer&amp;rdquo; from &amp;ldquo;reviser”.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The &lt;em&gt;reviewer&lt;/em&gt; node reads the necessary context and produces a structured list of issues,while the &lt;em&gt;reviser&lt;/em&gt; node sees only the affected slice plus those suggestions.&lt;/p&gt;&lt;h3 id=&#34;why-keep-them-separate&#34;&gt;Why keep them separate?&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audit clarity&lt;/strong&gt;&lt;br&gt;The reviewer’s JSON lives as its own artefact, so you can diff, grep, or hand-editthe feedback without touching the text itself. It&amp;rsquo;s also easier to audit automatic revisions and spot &amp;lsquo;additional helpfulness&amp;rsquo;.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Smaller prompts, cheaper calls&lt;/strong&gt;&lt;br&gt;A reviser that operates on &lt;em&gt;just the target part + suggestions&lt;/em&gt; uses far fewer tokensthan one that re-ingests the whole chapter.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Less collateral damage&lt;/strong&gt;&lt;br&gt;Narrow context means the reviser can’t “helpfully” rewrite good paragraphs in other sections or even just completely forget them.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;True parallelism&lt;/strong&gt;&lt;br&gt;Parts are context-isolated, so multiple revisions can run concurrently—no giant chapter-wide lock.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Targeted rollbacks&lt;/strong&gt;&lt;br&gt;If a revision introduces a new issue, it&amp;rsquo;s easy to rollback.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;what-the-flow-looks-like&#34;&gt;What the flow looks like&lt;/h3&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;code&gt;review_meaning_cohesion&lt;/code&gt; scans &lt;strong&gt;consistency_revised_stories&lt;/strong&gt;&lt;br&gt;and writes &lt;code&gt;meaning_cohesion_review_1.json&lt;/code&gt;:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;issues&amp;#34;&lt;/span&gt;: [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     {&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;part_number&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;suggestion&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Change sentence &amp;#39;...sentence...&amp;#39; to &amp;#39;...corrected sentence...&amp;#39; to ensure consistency with previous parts.&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;reason&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Inconsistent character name across parts.&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;severity&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;high&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     }&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;}&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;During the &lt;code&gt;revise_meaning_cohesion&lt;/code&gt; stage, revisions to specific parts are applied concurrently, e.g. reviser for part 8 only sees the text of &lt;strong&gt;part 8&lt;/strong&gt; and the suggestions for part 8.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&#34;other-novel-elements&#34;&gt;Other novel elements&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Stable DAG via no-op nodes&lt;/strong&gt; – skipping or caching never changes filenames or dependencies, so UI and controller logic stay simple.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Declarative &lt;code&gt;STAGE_DEPENDENCIES&lt;/code&gt;&lt;/strong&gt; – each endpoint validates its own inputs and fails fast if artefacts are missing.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Pluggable adapter subclasses&lt;/strong&gt; – swapping models or prompt strategies is a config change, not a refactor.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Direct-disk reading Server Side React Components&lt;/strong&gt; – Suprisingly trivial frontend code.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;SQLite error forensics&lt;/strong&gt; – a single query surfaces “prohibited-content” or other LLM failures.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Hot-reload mid-run&lt;/strong&gt; – tweak prompts or error handling while a 60-chapter fan-out is running; retries pick up the change without restart.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;WebP illustration storage&lt;/strong&gt; – generated art compresses very well; files are roughly 10× smaller than raw outputs.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;LLM-driven controller decisions&lt;/strong&gt; – the CLI uses the &lt;code&gt;review_overall&lt;/code&gt; output to decide whether to launch another revision round and which stages to skip.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&#34;personal-wins&#34;&gt;Personal wins&lt;/h2&gt;&lt;p&gt;The biggest win is defending my personal sanity.&lt;/p&gt;&lt;p&gt;I no longer have to babysit a set of fragile notebook based pipelines based on fallible and untrustworthy LLMs.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Higher confidence in quality&lt;/strong&gt; – every chapter passes level, consistency, meaning, and overall reviews—automatic multi-round fixes if needed.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Far less toil&lt;/strong&gt; – no more babysitting fragile notebooks; the pipeline self-checks and fails early.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Fast, parallel experimentation&lt;/strong&gt; – new adapters or prompts run side-by-side with hot-reload; iteration is “super fast and fun.”&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Cost flexibility&lt;/strong&gt; – total spend is higher (as there are more LLM calls), but the modular design lets me fall back to cheaper or local models whenever I choose.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;And the cool thing is that building this tooling was heavily accelerated by a coding assistant/agent, so it was significantly faster and more fun than I would have expected from the scope of the reengineering.&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Why I Pivoted StoryLearner Instead of Giving Up</title>
       <link>https://tinystruggles.com/posts/why_pivot_storylearner/</link>
       <pubDate>Sun, 18 May 2025 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/why_pivot_storylearner/</guid>
       <description>&lt;p&gt;I have a secret for you: the first version of &lt;a href=&#34;https://storylearner.app&#34;&gt;storylearner.app&lt;/a&gt; looked pretty different!&lt;/p&gt;&lt;p&gt;It had a pretty compelling story generation feature, where you could influence the story, pick the characters, and so on&amp;hellip;&lt;/p&gt;&lt;p&gt;It looked good — the illustrations were very compelling, and the stories were pretty fun. But I ripped it away. In this post, I’ll dive into how I realized I needed to make the change, what to change, and how I executed it.&lt;/p&gt;&lt;h3 id=&#34;the-first-reality-check-and-discouragement&#34;&gt;The First Reality Check and Discouragement&lt;/h3&gt;&lt;p&gt;In the beginning, I was riding the creative high of building something new. I had done a decent amount of research, and the space didn’t look too crowded. I felt motivated and inspired.&lt;/p&gt;&lt;p&gt;But over time, as I started talking to people and working on the product, the landscape began to shift.&lt;/p&gt;&lt;p&gt;I discovered tools like &lt;a href=&#34;https://www.languagereactor.com/&#34;&gt;Language Reactor&lt;/a&gt; and started getting aggressively targeted by competitor ads — apps I hadn’t even known existed before.&lt;/p&gt;&lt;p&gt;It was like pulling at a thread: the more I looked, the more players I uncovered, especially once I started working on the mobile alpha. That’s when it hit me — building something around stories wasn’t as novel as I’d thought, and the market was crowded.&lt;/p&gt;&lt;p&gt;That realization was discouraging, but not all at once.&lt;/p&gt;&lt;p&gt;I’d alternate between moments of &amp;ldquo;oh no&amp;rdquo; and renewed optimism: yes, the market is competitive, but I have a unique angle, right? That sustained me for a while.&lt;/p&gt;&lt;p&gt;But the final blow came when I uncovered a wave of mobile apps doing very similar things (like &lt;a href=&#34;https://readle-app.com/en/&#34;&gt;Redle&lt;/a&gt; - Rapidly Learn Languages with News &amp;amp; Stories). It was harder than ever to believe I had a differentiated product.&lt;/p&gt;&lt;p&gt;I also became increasingly skeptical about the viability of a subscription model in such a saturated space — people are tired of subscriptions. I started seriously considering quitting.&lt;/p&gt;&lt;h3 id=&#34;thought-process-behind-the-pivot&#34;&gt;Thought Process Behind the Pivot&lt;/h3&gt;&lt;p&gt;Selling individual books had crossed my mind before, but combining subscriptions and per-product purchases seemed too complex for the initial version of the product.&lt;/p&gt;&lt;p&gt;When the discouraging signs piled up, I took a step back. I wasn’t afraid to walk away — I’d had fun, I’d learned a lot, I’d shipped something. But I also had a nagging feeling that there was still something worth salvaging.&lt;/p&gt;&lt;p&gt;At that point, I had standalone stories, story packs, and a working prototype of a book adaptation pipeline. That pipeline was actually a pain to build — adapting books well is hard. You need to simplify language to the right CEFR level, maintain good translations, generate quality illustrations, and chunk everything into manageable parts. But that difficulty made it valuable.&lt;/p&gt;&lt;p&gt;Books are inherently engaging, and the idea of breaking them into short, predictable chapters — inspired by both Duolingo and Brandon Sanderson’s writing style (I&amp;rsquo;m a huge fan!) — made them even more learner-friendly.&lt;/p&gt;&lt;p&gt;So I made a call: drop the generative story feature, focus fully on adapted books, and build the product around that. It felt like a tighter, more focused bet.&lt;/p&gt;&lt;p&gt;Books are easier to understand and sell: people already look for language books. They can try a few chapters and decide whether they want to pay — no long-term commitment. That’s easier for users, even if it’s potentially worse for recurring revenue. But let’s be real — if no one subscribes, there’s no revenue anyway.&lt;/p&gt;&lt;h3 id=&#34;why-the-new-approach-is-better-and-more-promising&#34;&gt;Why the New Approach Is Better and More Promising&lt;/h3&gt;&lt;p&gt;The new direction is more aligned with how people actually think. When someone hears, &amp;ldquo;You can read &lt;em&gt;Sherlock Holmes&lt;/em&gt; in simplified Spanish, with sentence-by-sentence narration and illustrations, plus word lookups and review tools,&amp;rdquo; they get it. That’s an easy pitch.&lt;/p&gt;&lt;p&gt;Compare that to: &amp;ldquo;You can generate custom AI stories with predefined characters in various CEFR levels&amp;hellip;&amp;rdquo; It’s more abstract, the stories seem arbitrary. It sounds like yet another language learning gimmick, a thing that you probably don&amp;rsquo;t need.&lt;/p&gt;&lt;p&gt;Generative stories were fun and they looked good — hiding that feature wasn’t easy. But in practice, the value of curated, thoughtful content is higher.&lt;/p&gt;&lt;p&gt;It has more practical engineering benefits too. It’s reusable, more stable, and cheaper to generate at scale. With LLMs being flaky in both quality and uptime, having static, high-quality content is a strategic advantage.&lt;/p&gt;&lt;p&gt;Books also resonate emotionally. There’s a sense of identity and aspiration tied to finishing a real book — even a simplified one. And a known title gives people a reason to care.&lt;/p&gt;&lt;p&gt;Additionally, there was a pretty big difference in positioning that unlocked a new distribution channel. A learning app competes with the traditional language school, but a book? A book would be complementary, it doesn&amp;rsquo;t threaten the traditional teaching model. It adds an additional way to monetize learning to people who are already paying for learning. One important lesson I learned in the past is that selling to people who don&amp;rsquo;t want to spend money can be very frustrating, it&amp;rsquo;s much easier to sell to people who are already spending on something similar.&lt;/p&gt;&lt;h3 id=&#34;plan-and-execution-of-the-pivot&#34;&gt;Plan and Execution of the Pivot&lt;/h3&gt;&lt;p&gt;I planned the pivot in three parallel tracks: content, app changes, and marketing.&lt;/p&gt;&lt;p&gt;App changes, surprisingly, were faster than I expected. I was productive and used Copilot and Claude to speed things up. I restructured the app to make books first-class entities. I added audio narration support, Stripe integration for purchases, a chapter-based reader experience, and a reworked UX with a catalog and library view.&lt;/p&gt;&lt;p&gt;Then, I adapted three full books. That part turned out to be trickier than expected. LLM outputs needed QA and lots of checkpointing, sentence splitting caused bugs in TTS generation, and with 800MB per book (mostly images), even uploads became a challenge - one of the books had more than 700 hundreds of illustrations, and I also generated per sentence narration files. I had to parallelize file uploads and add more automated checks to make the pipeline robust.&lt;/p&gt;&lt;p&gt;Marketing and launch prep are still in progress. I created a new landing page and started working on blog content, but the real push is still ahead. I am also talking to people in real world and the response seems to be enthusiastic.&lt;/p&gt;&lt;h3 id=&#34;reflection&#34;&gt;Reflection&lt;/h3&gt;&lt;p&gt;Looking back, I’m glad I pivoted and didn&amp;rsquo;t give up.&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://storylearner.app&#34;&gt;Storylearner&lt;/a&gt; is a small, but real product — focused, scoped, and clear in what it offers. It won&amp;rsquo;t be a unicorn, but it doesn’t have to be. I&amp;rsquo;m still proud of it. The build is done, but can be extended and improved. Costs are low. Risk is capped.&lt;/p&gt;&lt;p&gt;The next phase is about getting it in front of people. Success will depend on distribution and my marketing skills. But at least now I’m selling something people intuitively understand. It also doesn’t compete with language schools — it complements them — which opens up potential partnerships and new distribution channels.&lt;/p&gt;&lt;p&gt;I’m cautiously optimistic. I didn’t quit. I refocused. I’m not tied down, and I still have upside. Wish me luck!&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>How I ended up building storylearner.app</title>
       <link>https://tinystruggles.com/posts/why_storylearner/</link>
       <pubDate>Sat, 17 May 2025 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/why_storylearner/</guid>
       <description>&lt;p&gt;I’ve always been drawn to self-directed learning. My interests have been pretty diverse - I would spend countless hours on math, science, soft-skills, movement skills (juggling, handstands) as well as languages.&lt;/p&gt;&lt;h2 id=&#34;a-long-standing-language-learning-obsession&#34;&gt;A long standing language learning obsession&lt;/h2&gt;&lt;p&gt;Over the years (starting as a teenager), I’ve seriously studied eight languages — not because I had to, but because I found it fun, challenging, and fascinating. Through trial and error, I’ve learned a lot about what actually works for adult learners learning on their own. And I also learned about all possible pitfalls, the main of them being, abandoning the project and losing all the progress due to forgetting.&lt;/p&gt;&lt;p&gt;But I&amp;rsquo;ve been successful in &lt;a href=&#34;https://www.storylearner.app/blog/my_journey_of_learning_spanish&#34;&gt;learning Spanish&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Reading, in particular, became my core of my learning. When I was &lt;a href=&#34;https://www.storylearner.app/blog/my_journey_of_learning_spanish&#34;&gt;learning Spanish&lt;/a&gt;, what helped most wasn’t drills or apps — it was regular reading. I started with graded readers and eventually moved on to novels. That reading habit didn’t just build vocabulary — it kept the language alive.&lt;/p&gt;&lt;p&gt;For most learners, jumping straight into native content is simply too hard. Instead, you can reach for graded readers - books adapted for your level.&lt;/p&gt;&lt;p&gt;But my experience with traditional graded readers wasn’t great. The selection was limited, the quality was often underwhelming, and there was no audio or pronunciation support. Later, I found other tools that approached reading through comprehensible input, but I wasn’t aware of them at the time. And even now, I think there’s still a gap: reading remains one of the most powerful ways to acquire a language — but if you aren&amp;rsquo;t fluent yet, it&amp;rsquo;s rough out there.&lt;/p&gt;&lt;p&gt;That gap stuck with me. Especially as I started learning Portuguese about two years ago — which, unlike Spanish, presented real pronunciation challenges for me. I had already experimented with TTS in a side hustle and taken several deep learning courses, so I started thinking: what could I build to actually help?&lt;/p&gt;&lt;h2 id=&#34;struggle-to-find-something-that-deeply-resonates&#34;&gt;Struggle to find something that deeply resonates&lt;/h2&gt;&lt;p&gt;At the same time, I was in a &lt;a href=&#34;https://dogpatchlabs.com/founders/&#34;&gt;Founders Startup Accelerator&lt;/a&gt;, exploring B2B ideas and working with different cofounders. I kept chasing what the program was designed to produce — a validated, fundable B2B startup.&lt;/p&gt;&lt;p&gt;It was intense - lots of brainstorming, early excitement, and the slow crush of reality when things proved harder than expected. I struggled to get a personal fit with a validated business problem. After parting ways with my second cofounder - who was amazing, but very focused on the industry that I didn&amp;rsquo;t care deeply about, I decided to stop forcing it and just build something that genuinely excited me.&lt;/p&gt;&lt;h2 id=&#34;leaning-into-my-genuine-excitement&#34;&gt;Leaning into my genuine excitement&lt;/h2&gt;&lt;p&gt;So I started experimenting with a story-based reading tool. I wasn’t committing to it — just exploring something that felt fun and energizing.&lt;/p&gt;&lt;p&gt;Then a funny thing happened: I had planned a two-week paragliding trip in the south of Spain… (with the intention to take a step back from B2B SaaS and catching sunshine) and it rained the whole time. Paragliding was cancelled, so I rerouted to Málaga, found a coworking space called &lt;a href=&#34;https://tlr-coworking.com/&#34;&gt;&lt;strong&gt;The Living Room&lt;/strong&gt;&lt;/a&gt;, and settled in. I ended up spending two very productive weeks there — surrounded by friendly nomads, good energy, and people who were genuinely curious about the project. In that time, I made massive progress and storylearner started to take shape.&lt;/p&gt;&lt;p&gt;Later, I paused the project again to try one last cofounder collaboration. But it didn’t lead anywhere, and by the time the investment committee pitch came around, it was too late to get traction on anything new. So I returned to storylearner — not as a fallback, but because it was the one thing I &lt;em&gt;wanted&lt;/em&gt; to keep building.&lt;/p&gt;&lt;p&gt;What started as a light experiment quickly turned into weeks of deep development. I built an AI-powered content pipeline to adapt public domain books into simplified, bite-sized chapters with sentence-level audio and illustrations. Along the way, I pivoted from generic story generation to &lt;strong&gt;adapted books&lt;/strong&gt; — a format that’s structured, engaging, and rich in educational value.&lt;/p&gt;&lt;h2 id=&#34;why-it-fits&#34;&gt;Why it fits?&lt;/h2&gt;&lt;p&gt;I’ve spent most of my career working in backend and infrastructure. I enjoy serious, deep engineering, but at the same time I really like building customer facing products. It&amp;rsquo;s an itch that I like to scratch. I’ve built up full-stack skills through side projects.&lt;/p&gt;&lt;p&gt;Storylearner is something I can build solo, keep costs low, and ship quickly - it gives me space to live my life and explore other ideas as well. But more importantly, it brings together the things I actually care about — language learning, AI, reading, and creating tools that support growth.&lt;/p&gt;&lt;p&gt;I also believe there’s a real opportunity here. GenAI is incredibly powerful, but it still involves a lot of DIY friction. Meanwhile, tools like Duolingo succeed because they make learning smooth, convenient, and even addictive. I think &lt;strong&gt;reading&lt;/strong&gt; deserves the same treatment — and I want to help make it easier, more attractive, and more rewarding for language learners. Especially by unlocking the &lt;strong&gt;goldmine of public domain books&lt;/strong&gt; that are just waiting to be adapted.&lt;/p&gt;&lt;h2 id=&#34;-where-this-fits-into-my-life&#34;&gt;🧭 Where this fits into my life&lt;/h2&gt;&lt;p&gt;Storylearner is one of several things I’m exploring right now. After years of full-time work, I’ve intentionally made space to pursue projects that feel meaningful and energizing. I’m interested in learning, in building tools that support human growth, and in seeing what’s possible with new technologies.&lt;/p&gt;&lt;p&gt;I’ve already started exploring another interest area in parallel — but storylearner isn’t “just a little project.” It’s a &lt;strong&gt;serious bet in my portfolio of bets&lt;/strong&gt;. One I believe in. One I want to see succeed commercially, not just creatively. I’m proud of what I’ve built, and I’m excited to see who it resonates with.&lt;/p&gt;&lt;h2 id=&#34;-what-next&#34;&gt;🫱 What next?&lt;/h2&gt;&lt;p&gt;I believe storylearner is valuable — now I need to find the people it’s &lt;em&gt;for&lt;/em&gt;.&lt;/p&gt;&lt;p&gt;The core experience is live: adapted books with interactive reading support and audio, designed to make language learning through stories more fun, more sustainable, and more effective.&lt;/p&gt;&lt;p&gt;I&amp;rsquo;m a developer, I could easily keep building and building. Selling and marketing goes deeply against my instincts, but building without users is a common trap for engineers. So for now I am forcing myself to stop building until I find enough traction — I&amp;rsquo;m looking for &lt;strong&gt;early adopters&lt;/strong&gt; who see the value and want to be part of it.&lt;/p&gt;&lt;p&gt;My next goal is simple: &lt;strong&gt;reach my first 100 sales&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;If you&amp;rsquo;re learning &lt;strong&gt;Spanish, Portuguese (Portugal or Brazil), French, or Polish&lt;/strong&gt; — or know someone who is — try it out. Buy a book, read a chapter, see how it feels, let me know how it goes! You’ll not only support the project — you’ll be part of shaping something that’s trying to make language learning better, through the joy of stories.&lt;/p&gt;&lt;p&gt;→ &lt;a href=&#34;https://storylearner.app&#34;&gt;https://storylearner.app&lt;/a&gt;&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>3 Reflections from a sabbatical after Google</title>
       <link>https://tinystruggles.com/posts/sabbatical/</link>
       <pubDate>Sat, 15 Mar 2025 10:45:59 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/sabbatical/</guid>
       <description>&lt;p&gt;I quit Google at the end of July 2024 and I took a work break until mid-January 2025 (when I joined a startup accelerator full time). It would be a miss if I didn’t reflect on my sabbatical experience!&lt;/p&gt;&lt;p&gt;I will describe what I was up to in another post, but in short it was a mix of traveling and hanging out at my home base in Dublin. Here my 3 main reflections!&lt;/p&gt;&lt;h2 id=&#34;not-working-turned-out-to-be-okay&#34;&gt;Not working turned out to be okay&lt;/h2&gt;&lt;p&gt;I was concerned about how I will feel about not having a job and how others will react about me not working.&lt;/p&gt;&lt;p&gt;Why would I be concerned?Financial instability: No paycheck coming in.Lack of structure for my days and tasks and accountability from others: Would I get lethargic and waste a lot of time?Uncertainty about the future: Off a structured path so unclear what will likely happen next.&lt;/p&gt;&lt;p&gt;All of the above could make me uncomfortable. The uncertainty, the fear about the future, the lack of direction…&lt;/p&gt;&lt;p&gt;How was I? Surprisingly, pretty great.&lt;/p&gt;&lt;p&gt;I wasn’t worried about finances, because I have worked on my financial mindset for many years. I had a ton of cash in my checking account, enough to cover multiple years of my burn rate. AndI was resolved to not worry about spending it. I gave myself a generous budget, similar to what I was spending while employed and at times higher.&lt;/p&gt;&lt;p&gt;This is obviously a very privileged position, but I strongly believe that how we feel about money is only slightly correlated with our financial situation. There are many people sitting on huge savings and investments who are afraid to pull the trigger. There are also people with barely any money making big bets on themselves, figuring things out as they go.&lt;/p&gt;&lt;p&gt;Being resourceful is more powerful than just having resources.&lt;/p&gt;&lt;p&gt;Was lack of structure or accountability a problem? No. I am naturally very disciplined and a good planner. Both traveling and sitting at home - I was never bored, always had a lot of things to do and rarely lacked enthusiasm. Occasionally lacked energy due to doing too much cool stuff or being sick.&lt;/p&gt;&lt;p&gt;I trust my body that if I am lacking enthusiasm to do things I either need to rest more or find better things to do. I might not want to do things 100% (like a run in the rain!), but at least part of me needs to want it.&lt;/p&gt;&lt;p&gt;Other people were very understanding of the idea of taking a career break. In their eyes “I deserved it”, in their eyes “I got this” and that shouldn’t set me back.&lt;/p&gt;&lt;h2 id=&#34;my-social-life-changed-for-the-better&#34;&gt;My social life changed for the better&lt;/h2&gt;&lt;p&gt;During this sabbatical I was living on my own. When I stopped going to work I ended up without  having people around me by default, which was a new thing for me.&lt;/p&gt;&lt;p&gt;While working I would always have coworkers and I would usually work from the office. During the forced work from home period, I had my partner there.&lt;/p&gt;&lt;p&gt;When you have a job, you have companionship of people physically around you. You have meetings. You are forced to interact, but you don’t have much choice with whom. Most of my career I had a pretty good set of coworkers. I would get along fine with most and I would usually really like some. Sometimes I worked with my very good friends. But sometimes it would be mostly professional.More capacity for intentional socializing&lt;/p&gt;&lt;p&gt;During my sabbatical I replaced the shallow level socialization from work with a deep and meaningful intentional socialization with my friends. I was also more open to making new friends and I had the capacity to do that.&lt;/p&gt;&lt;p&gt;While working, I wouldn’t do a lot of meeting people for deep conversations after work during the work week. I would either do some sport (e.g. climbing) and have some shallow social interactions there or I would need some alone time to relax or process what has been going on.&lt;/p&gt;&lt;p&gt;While not working, I had much more social energy, I would be up to meet for lunches, coffees, dinners and activities. And I was lucky to have many friends to spread this social load over. I had a lot of amazing 1:1 time as well as very fun group hangouts.&lt;/p&gt;&lt;p&gt;My lack of constraints also allowed me to bond deeper with people through travel. I met really interesting people by traveling to cool events (like Zuzalu Village in Georgia) and I went on several trips when I spent extended amounts of time with various friends which really helped me deepen those friendships.&lt;/p&gt;&lt;h3 id=&#34;intentional-socializing-is-much-better&#34;&gt;Intentional socializing is much better&lt;/h3&gt;&lt;p&gt;While working I would occasionally have those deflationary social interactions - when someone’s lack of energy or enthusiasm would drag me down with them.&lt;/p&gt;&lt;p&gt;My sabbatical gave me much more control of my social environment and I ended up developing my relationships and feeling very connected even though I would spend more time alone.I had more energy for meaningful socializing 1:1, I would be much more proactive in organizing it and the quality of the interactions was much higher.&lt;/p&gt;&lt;h3 id=&#34;connecting-when-people-are-busy-with-work&#34;&gt;Connecting when people are busy with work?&lt;/h3&gt;&lt;p&gt;A surprising amount of people are available during the work week for lunches, coffees during the work day or breakfasts or just a random phone call.&lt;/p&gt;&lt;p&gt;There are many people who don’t work or have weird schedules, you will start meeting them once you are on a weird schedule.&lt;/p&gt;&lt;p&gt;If not enough people want to hang out with you or have availability to do it, you might need new friends. That sounds tough, but wait. You are in luck, if you are not working, you have much more capacity to explore for new friends and invest in those friendships.&lt;/p&gt;&lt;p&gt;When you travel, there are likely other travelers you could meet. People traveling solo are especially willing to connect.&lt;/p&gt;&lt;p&gt;There are various events you can join, social sports classes, and even coworking spaces (you can “work&amp;quot; on your own project there). Show up, be friendly and exchange contacts, it really starts that simple.&lt;/p&gt;&lt;h2 id=&#34;i-redefined-what-productivity-meant-to-me&#34;&gt;I redefined what productivity meant to me&lt;/h2&gt;&lt;p&gt;One of my guiding principles for that sabbatical was “not rushing”. I also deliberately didn’t plan any major projects, if it felt right, I would do it, if I didn’t, fine, I would let it go.&lt;/p&gt;&lt;h3 id=&#34;unreasonable-non-work-expectations&#34;&gt;Unreasonable non work expectations&lt;/h3&gt;&lt;p&gt;I would consider myself a driven, ambitious and disciplined person. I was used to getting a lot done: my job, lots of workouts, side projects, doing courses, hobbies, the list goes on and on.&lt;/p&gt;&lt;p&gt;I was aware that what I wanted was ambitious, but it also felt achievable. I wanted it all.&lt;/p&gt;&lt;p&gt;Turns out that I have trouble keeping up with all my goals and expectations even if I don’t work.&lt;/p&gt;&lt;p&gt;I am not dumb, I swear. I prioritize, I kill projects and say no to people. But still, I am sometimes overly optimistic about my abilities to keep taking on more hobbies, healthy routines and projects.&lt;/p&gt;&lt;p&gt;It makes me realize that I was operating in a very unsustainable way and I have to lower my productivity expectations.&lt;/p&gt;&lt;h3 id=&#34;chronic-stress&#34;&gt;Chronic stress&lt;/h3&gt;&lt;p&gt;During my work life I would often feel stressed and overwhelmed. It wouldn’t be very severe, but enough for me to be aware of the looming tension.&lt;/p&gt;&lt;p&gt;I would also struggle with sleep, especially waking up during the night and waking up too early - often with intense thoughts about work.&lt;/p&gt;&lt;p&gt;I would usually fall asleep fine, probably due to being exhausted, and if that didn’t work very well, I would take melatonin and that would usually suffice.&lt;/p&gt;&lt;p&gt;I thought that maybe it was anxiety? But researching the topic made me realize that it wasn’t anxiety. I didn’t have a worrying habit, I would just feel a lot of tension and unease - and that was stress from having insufficient resources to meet all the expectations I had.&lt;/p&gt;&lt;p&gt;I know, semantics, but for me realizing that distinction was meaningful as it helped me diagnose the cause.&lt;/p&gt;&lt;p&gt;I also had a major breakup that flipped my life upside down. I started my sabbatical a year after that, but I think I was still somewhat affected.&lt;/p&gt;&lt;p&gt;I have developed a toolbox for dealing with stress during my work at Google and it was very helpful, but still my problems with sleep were persistent. I also had various tools and habits to optimize my sleep, which I was using consistently. I tested many of them and I was very disciplined about following the ones that worked well for me.&lt;/p&gt;&lt;p&gt;Funnily enough, after my stress melted over a couple of months, my sleep improved. That happened, even though my sleep hygiene habits got worse.&lt;/p&gt;&lt;h3 id=&#34;deprogramming-the-hard-work-culture&#34;&gt;Deprogramming the “hard work” culture&lt;/h3&gt;&lt;p&gt;I come from a family of hard working people and from a culture that values hard work and effort. I have always seen a value in working smarter, not harder. But at the same time, in my head, you should still work kinda hard, just use your smarts to get X amount more impact, instead of scaling down your efforts to 1 / X.&lt;/p&gt;&lt;p&gt;I think my programming goes like this: hard work is a moral obligation. Some people are compensated more than others (sometimes due to luck, not always on merit), but everyone should work hard to be contributing. It’s like fairness of effort based on everyone being obligated to give their best. Slacking off when other people are working hard is unfair and immoral.&lt;/p&gt;&lt;p&gt;Of course the reality is much more complex than the above!And this is just one set of beliefs. Another set of beliefs could be that I have one life and I should do the best I can to enjoy it. Working and helping others can be a source of meaning and satisfaction that can be one of the sources of my happiness, so I can do that for that. Otherwise I should use work as a means to get enough resources to enjoy life. If I gathered enough money, then it means that I produced enough value for that time. No need to earn my keep everyday.&lt;/p&gt;&lt;h3 id=&#34;internal-productivity&#34;&gt;Internal productivity&lt;/h3&gt;&lt;p&gt;Recently I was watching a lecture about writing by Brandon Sanderson and he said something along the lines of:“You are the main product of your writing, practice of writing changes you and makes you a better writer, a different person”. So it doesn’t matter that you will write books that won’t be published. Do the activity for how it will change you.&lt;/p&gt;&lt;p&gt;I found it very compelling.&lt;/p&gt;&lt;p&gt;Ok, it is kinda obvious. You learn skills to have skills - it changes you. You work out to change your body (or keep it up), it changes you! Sometimes you do that to achieve an external goal like impress others or get a job. But it’s more interesting if it’s purely for you.&lt;/p&gt;&lt;p&gt;Internal productivity is when you are the main product of your activities, when they change you.&lt;/p&gt;&lt;p&gt;And it doesn’t have to matter to anyone but you!&lt;/p&gt;&lt;p&gt;My sabbatical was full of internal productivity.&lt;/p&gt;&lt;h3 id=&#34;quality-of-life-vs-quality-of-the-experiencing-of-life&#34;&gt;Quality of life vs Quality of the experiencing of life&lt;/h3&gt;&lt;p&gt;I think that everyone will agree that ‘quality of life’ matters. But you might be living a “high quality life”, but be bad at experiencing it.&lt;/p&gt;&lt;p&gt;Rushed, detached, reactive, struggling to be present. We’ve all been there.&lt;/p&gt;&lt;p&gt;I have a strong desire to be good at experiencing my life. I want to be present in my experiences as well as process them afterwards and I want to understand both myself and the world as much as possible. I want to live an examined life.&lt;/p&gt;&lt;p&gt;“Oh, this person must live an examined life” - said no one, ever.It’s hard to pinpoint if people take time to do the internal work that is required. Maybe they are simply not sharing it? But I think it catches up to people.&lt;/p&gt;&lt;p&gt;And all this examination takes time! Time that could be spent on more grind that could be yielding visible results! But I think that I worth it, and I will treat it as productive.&lt;/p&gt;&lt;p&gt;So even though I had many days that were neither “productive” or “instagrammable” or “full of stories”. I will never regret time spent on:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Being in touch with myself, my feelings, thought patterns and opinions&lt;/li&gt;&lt;li&gt;Deep conversations - prioritizing them and trying to improve at them&lt;/li&gt;&lt;li&gt;Learning and reflecting&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;conclusions&#34;&gt;Conclusions&lt;/h2&gt;&lt;p&gt;It felt crazy to quit. But I am glad I did. I believe in closing a chapter to start another one. But sometimes you need a break between adventures. And this was a very good one.&lt;/p&gt;&lt;p&gt;It turned out to be ok to not work for a while and have an unrushed and unproductive period. Even if by the end of it I redefined what productivity meant to me!&lt;/p&gt;&lt;p&gt;I had a lot of fun and intense adventures, but I am really glad for the &amp;ldquo;boring moments&amp;rdquo;.&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>How to transcribe long audios fast with open source (colab included)</title>
       <link>https://tinystruggles.com/posts/fast_long_audio_transcriptions/</link>
       <pubDate>Mon, 14 Oct 2024 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/fast_long_audio_transcriptions/</guid>
       <description>&lt;p&gt;In this post I will give you &lt;a href=&#34;https://github.com/ilonajulczuk/podcast_takeways/blob/main/drive_transcriber.ipynb&#34;&gt;code&lt;/a&gt; that you can run yourself in &lt;a href=&#34;https://colab.research.google.com/&#34;&gt;Colab&lt;/a&gt; (or on your own machine with a GPU), that will allow you to very quickly transcribe long audios in many languages.&lt;/p&gt;&lt;p&gt;The setup uses the whisper model from huggingface, Google Drive and Colab.&lt;/p&gt;&lt;p&gt;I used it to transcribe multi hour podcasts within a couple of minutes (about 1-5 minutes depending on the length).&lt;/p&gt;&lt;h3 id=&#34;motivation&#34;&gt;Motivation&lt;/h3&gt;&lt;p&gt;Some time ago I was getting frustrated that the podcasts that I was listening to didn&amp;rsquo;t have transcriptions available.&lt;/p&gt;&lt;p&gt;Let&amp;rsquo;s pick this &lt;a href=&#34;https://podcastnotes.org/huberman-lab/dr-charan-ranganath-how-to-improve-memory-focus-using-science-protocols-huberman-lab/&#34;&gt;one&lt;/a&gt; as an example - it&amp;rsquo;s a massive, almost 3 hour long podcast by Huberman. It&amp;rsquo;s knowledge packed!&lt;/p&gt;&lt;p&gt;Could I get the transcription? It&amp;rsquo;s not included anywhere, but I could make my own.&lt;/p&gt;&lt;p&gt;Sure, there are services for it, one that looked promising to me is &lt;a href=&#34;https://www.listen411.com/about/&#34;&gt;listen411&lt;/a&gt; - it charges 0.06 USD for 1 minute of audio summarization + 1 USD per file, interesting!&lt;/p&gt;&lt;p&gt;A podcast like one I was interested in would cost me about 13 USD, because it&amp;rsquo;s a very long one. I probably listen to 5 podcasts per week or more, so let&amp;rsquo;s say 20 per month. Ugh, the costs could add up fast.&lt;/p&gt;&lt;p&gt;If you go one level down, there are also APIs from cloud providers, which could be a good alternative. I checked the costs of transcription in google and aws: 0.024 USD or 0.016 USD per minute respectively, not bad, but it&amp;rsquo;s still about 1-2 USD per long podcast.&lt;/p&gt;&lt;p&gt;So I wondered, can I do better myself? Based on my knowledge of the current state of the ML, audio transcription is a pretty much a solved problem and there are excellent models available publicly for free. With open source libraries and Colab (easily accessible GPUs) I could build a DIY solution that would be much cheaper.&lt;/p&gt;&lt;h3 id=&#34;cost-of-running-the-diy-solution&#34;&gt;Cost of running the DIY solution&lt;/h3&gt;&lt;p&gt;The costs of the DIY solution are pretty much just the Colab running costs. When you break it down, it&amp;rsquo;s about 3 orders of magnitude cheaper - provided that it&amp;rsquo;s a fast process! It can also be totally free if you use the free tier of the Colab service.&lt;/p&gt;&lt;p&gt;Colab has a free tier that I think could be sufficient for that, but if you want a more guaranteed access to resources, you can use the paid version of Colab as well. the Colab GPU pricing is 0.196/hr USD (T4 as of 2024).&lt;/p&gt;&lt;p&gt;A cost of ~(one podcast) transcription - ~3minutes GPU time (T4) - approximately 0.01 USD.&lt;/p&gt;&lt;p&gt;And if you were to scale it, you could also run on (self) hosted GPU driving the cost even lower.&lt;/p&gt;&lt;h3 id=&#34;obstacles-on-the-way&#34;&gt;Obstacles on the way&lt;/h3&gt;&lt;p&gt;The code that I got at the end is very fast and provides high quality results, but it took some trial and error to get things right&amp;hellip;&lt;/p&gt;&lt;p&gt;Major issues I encountered were slowness and poor quality of results.&lt;/p&gt;&lt;p&gt;I hate slow running models, I have no patience for long running pipelines - it also gets expensive fast as you pay for the computation time.&lt;/p&gt;&lt;p&gt;My plan was to use huggingface (as it&amp;rsquo;s a great ecosystem) and I used a &lt;a href=&#34;https://huggingface.co/learn/audio-course/en/chapter7/transcribe-meeting&#34;&gt;tutorial&lt;/a&gt; with speaker diarization as a starting point. Dirarizaton lets you annotate the transcription with different speakers and when they speak automatically. It appeared relevant and promising.&lt;/p&gt;&lt;p&gt;But in practice copying the approach with diarization wasn&amp;rsquo;t feasible due to slowness for audios longer than 5 minutes. It just seemed stuck for hours without showing much progress. I spent quite a long time waiting for the transcriptions.&lt;/p&gt;&lt;p&gt;I also spend a good bunch of time fiddling around and learning API details of hugging face libraries, that tutorial confused me more than it helped.&lt;/p&gt;&lt;p&gt;I wondered if the models I was using were too expensive to run and I played with different models trying to assess if I can get my transcriptions faster, and some results weren&amp;rsquo;t the best&amp;hellip;&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/repeated.png&#34; alt=&#34;Repeated&#34;&gt;&lt;/p&gt;&lt;p&gt;In the end, the breakthrough resource that helmed me was studying the &lt;a href=&#34;https://github.com/Vaibhavs10/insanely-fast-whisper/tree/main&#34;&gt;Insanely Fast Whisper repository&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Especially this &lt;a href=&#34;https://github.com/Vaibhavs10/insanely-fast-whisper/blob/main/notebooks/infer_transformers_whisper_large_v2.ipynb&#34;&gt;notebook&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Insights from that notebook were enough to simplify my code greatly and speed it up dramatically!&lt;/p&gt;&lt;h2 id=&#34;diy-solution&#34;&gt;DIY solution&lt;/h2&gt;&lt;h3 id=&#34;the-setup&#34;&gt;The setup&lt;/h3&gt;&lt;p&gt;The overall solution I built and I&amp;rsquo;m sharing is:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;upload audio files to a personal Google Drive Directory (either manually or another script, e.g. RSS feed client) - the directory is called &lt;code&gt;audiotranscriptions&lt;/code&gt;&lt;/li&gt;&lt;li&gt;mount Google Drive in Colab and scan the directory for audio files missing a transcription file&lt;/li&gt;&lt;li&gt;transcribe audios that needed transcriptions and save transcriptions as txt files in Google Drive&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/audiotranscriptions.png&#34; alt=&#34;transcriptions&#34;&gt;&lt;/p&gt;&lt;h3 id=&#34;transcription-logic&#34;&gt;Transcription logic&lt;/h3&gt;&lt;p&gt;Without performance optimizations the code is as simple as:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; torch&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; transformers &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; pipeline&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pipe &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pipeline(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;automatic-speech-recognition&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;openai/whisper-large-v2&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                device&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;cuda:0&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;filename &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;./myfile.mp3&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;transcribed_text &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pipe(filename)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As the pipeline is so simple and it uses whisper (state of the art model!), it&amp;rsquo;s already pretty good, but it can be improved.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;using half precision - this will let the model use less GPU memory&lt;/li&gt;&lt;li&gt;chunking and batching - chunking will dramatically decrease complexity as the computation time usually is usually not linearly proportional to the duration, but a higher order polynomial. Chunking puts the limit on that time, it&amp;rsquo;s like processing a bunch of smaller audios and stitching them together. Batching helps to parallelize that. Additionally batching goes hand in hand with using lower precision as if a single batch takes less memory, you can have more batches at once.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The code modifications are very simple.&lt;/p&gt;&lt;p&gt;Enabling half precision:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; torch&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; transformers &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; pipeline&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pipe &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pipeline(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;automatic-speech-recognition&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;openai/whisper-large-v2&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                torch_dtype&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;torch&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;float16,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                device&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;cuda:0&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Enabling batching and chunking:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;filename &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;./myfile.mp3&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;transcribed_text &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pipe(filename,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        chunk_length_s&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        batch_size&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;16&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                        return_timestamps&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;helper-logic&#34;&gt;Helper logic&lt;/h3&gt;&lt;p&gt;The rest of the code from the attached notebook glues it all together. Here are the descriptions of the main parts.&lt;/p&gt;&lt;p&gt;Mount the Google Drive and navigate to the directory &lt;code&gt;audiotranscriptions&lt;/code&gt; containing files needing transcriptions:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; google.colab &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; drive&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;drive&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;mount(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;/content/drive&amp;#39;&lt;/span&gt;, force_remount&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt;cd &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;content&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;drive&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;MyDrive&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;audiotranscriptions&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Utility functions for the logic if the file needs a transcription:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; pathlib &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Path, PosixPath&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get_file_prefix&lt;/span&gt;(filename: PosixPath):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; filename&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;split(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;.&amp;#34;&lt;/span&gt;)[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;needs_transcription&lt;/span&gt;(audio: PosixPath, transcriptions: set[str]):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; get_file_prefix(audio) &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; transcriptions&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;transcribe_file&lt;/code&gt; is calling the previously created pipeline for a given audio &lt;code&gt;filename&lt;/code&gt; and saves the transcription under &lt;code&gt;transcription_filename&lt;/code&gt;.&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;transcribe_file&lt;/span&gt;(pipe, filename, transcription_filename):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;transcribing&amp;#34;&lt;/span&gt;, filename)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  outputs &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pipe(filename,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                chunk_length_s&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                batch_size&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;16&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                return_timestamps&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  text &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; outputs[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;text&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; open(transcription_filename, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;w&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; f:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write(text)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;transcribe_all&lt;/span&gt;(pipe, audios, transcriptions):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; audio &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; audios:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; needs_transcription(audio, transcriptions):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      transcription_filename &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; audio&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;with_suffix(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;.txt&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      transcribe_file(pipe, audio&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name, transcription_filename)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      transcriptions&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;add(get_file_prefix(audio)) &lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Putting it together:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;scan the directory&lt;/li&gt;&lt;li&gt;find audio and text files&lt;/li&gt;&lt;li&gt;transcribe whatever is needed&lt;/li&gt;&lt;/ul&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;files &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Path&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;cwd()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;iterdir()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;audios &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [file &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; file &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; files &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; file&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;suffix &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;.mp3&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;.wav&amp;#34;&lt;/span&gt;]]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;transcriptions &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {get_file_prefix(file) &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; file &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; Path&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;cwd()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;iterdir() &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; file&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;suffix &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;.txt&amp;#34;&lt;/span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;transcribe_all(pipe, audios, transcriptions)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;code--results&#34;&gt;Code &amp;amp; results&lt;/h3&gt;&lt;p&gt;You can see the colab notebook &lt;a href=&#34;https://github.com/ilonajulczuk/podcast_takeways/blob/main/drive_transcriber.ipynb&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I have tested the script on podcasts using English as well as Spanish and Polish and the transcriptions looked very good to my naked eye. I haven&amp;rsquo;t done a deeper quality assessment.&lt;/p&gt;&lt;h3 id=&#34;whats-next&#34;&gt;What&amp;rsquo;s next?&lt;/h3&gt;&lt;p&gt;After you have the text, the fun just starts!&lt;/p&gt;&lt;p&gt;I have further used LLMs to strip the transcripts from promotional content, make summaries and extract wisdom out of the longer audio. But that is a topic for another article!&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>I killed my side projects - clean slate</title>
       <link>https://tinystruggles.com/posts/rip_side_projects/</link>
       <pubDate>Sun, 15 Sep 2024 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/rip_side_projects/</guid>
       <description>&lt;p&gt;I have recently shut down the indie products I’ve been accumulating over the years, RIP:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;redeal.app - real estate investment calculator - inspired by bigger pockets, initially scraping daft (irish real estate listing website)&lt;/li&gt;&lt;li&gt;invertimo.com - investment tracking and bookkeeping (open source), useful for stocks, funds or any sort of crypto assets if multiple exchanges/etc are used, was integrated with degiro, interactive brokers and binance… + manual uploads&lt;/li&gt;&lt;li&gt;watchlimits.com (that is partially alive as the last version is still available in the chrome store, but the ‘premium features’ won’t be working) - a productivity extension helping with excessive video watching online&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I lost motivation to work on these projects, they were either in zombie stage (Redeal/Invertimo) or in somewhat alive stage (watchlimits), but little momentum or financial potential. I would occasionally get some support/feature requests for watchlimits, but I didn’t want to throw good money after bad (in terms of my productive project time).&lt;/p&gt;&lt;p&gt;And there were also server costs and upkeep costs of my time to get everything running smoothly and securely. Additionally there was guilt from not providing a good product experience as I wasn’t motivated to deliver fixes or improvements.&lt;/p&gt;&lt;p&gt;So I pulled the plug! Servers turned down. I didn&amp;rsquo;t bother in open sourcing closed source stuff or selling the projects.&lt;/p&gt;&lt;h2 id=&#34;joy-of-creating-vs-burden-of-maintaining&#34;&gt;Joy of creating vs burden of maintaining&lt;/h2&gt;&lt;p&gt;There are people who are great builders and there are people who are great operators (or maintainers or gardeners). I am a capable operator, but it has to be related to something that I really believe in. I&amp;rsquo;m not internally motivated by the activity itself. While I can create stuff, just for the joy of creating.&lt;/p&gt;&lt;p&gt;I had a pretty strong creative impulse to build side projects over the years. I had a dream of having my software company for some time, but it’s a pretty difficult task. I also didn’t have very good opportunities to build products e2e at work, so building complete products as side projects was a good outlet. It was a nice way to experiment with UX, tech, product design, marketing - all on the side of working in a pretty demanding job.&lt;/p&gt;&lt;p&gt;And I was somewhat successful, because I delivered 3 project MVPs - Redeal (real estate investment), Invertimo (stock investment tracking and book keeping), Watchlimits (productivity chrome extension). All got some users who found value in it. With watchlimits getting some payments and reaching almost 1k of free active users (and more installs).&lt;/p&gt;&lt;p&gt;I learned quite a lot about building a product, getting feedback, marketing, I got confidence in the fact that I can build stuff - even if part time and it could be better it was good enough to execute the vision, the technical side was never a blocker for my projects.&lt;/p&gt;&lt;p&gt;But what these projects had in common was that they had very little potential or momentum to become profitable. I also wasn’t that passionate about any of the topics. The itch was scratched, the products remained alive, but they were pretty much in zombie mode.&lt;/p&gt;&lt;h2 id=&#34;why-didnt-i-shut-down-the-projects-the-moment-i-stopped-believing-in-them&#34;&gt;Why didn’t I shut down the projects the moment I stopped believing in them?&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;Cleanup is work and it’s somewhat tedious&lt;/li&gt;&lt;li&gt;Emotional attachment&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;To properly unwind the project I would have to put in quite a lot of hours and I am chronically overscheduled - even if it’s a spontaneous week of windsurfing - it’s hard to fit in a bunch of productive project hours for a thing that I don’t really want to do. It&amp;rsquo;s very easy to procrastinate.&lt;/p&gt;&lt;p&gt;And there is an emotional load of killing your baby, even if you stopped caring about the baby - you feel bad about both not caring about your baby and then killing your baby project.&lt;/p&gt;&lt;h2 id=&#34;clinging-after-stopped-developing&#34;&gt;Clinging after stopped developing&lt;/h2&gt;&lt;p&gt;Some thoughts that were at the back of my mind:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;“Maybe it will still work out?”&lt;/li&gt;&lt;li&gt;“Maybe I will feel like working on it again?”&lt;/li&gt;&lt;li&gt;“It&amp;rsquo;s proof that I can build things and release things, right?”&lt;/li&gt;&lt;li&gt;“What will people who observed my journey think?”&lt;/li&gt;&lt;li&gt;“It’s not expensive to keep around… some people have value from it”&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I never had any solid conviction in my project watchlimits. From the start it was “let’s do this as a learning project and carry on working on it until I feel like it or find a better thing to work on”. It started during a hackathon. I had a strong desire to have a project to work on, but finding the right idea was hard, talking to prospective customers was hard, so I did what I knew how to do… jumped into the building process.&lt;/p&gt;&lt;p&gt;Doing back of the napkin calculations only confirmed my hunches. It was very unlikely that the project was financially viable. But I tried! I tried a lot of stuff and I learned pretty well how it feels to release a product with little financial traction.&lt;/p&gt;&lt;p&gt;After I couldn’t really convert people to paid, I made the ‘premium’ features free. It was somewhat disheartening that after I put effort into building the premium features almost no one was using it. After opening the features, people did actually find value in them and it was making me happy for a while.&lt;/p&gt;&lt;p&gt;And the user base has grown roughly from a hundred to about 1k active users and they started giving me more and more requests and at some point I just stopped working on it entirely. My heart wasn’t in it.&lt;/p&gt;&lt;h2 id=&#34;moving-on&#34;&gt;Moving on&lt;/h2&gt;&lt;p&gt;It feels good to have a clean slate. The limbo of &amp;ldquo;it&amp;rsquo;s still somewhat alive, but I don&amp;rsquo;t want to work on it&amp;rdquo; was taking a toll on me. I want to spend time on projects that I find interesting, exciting, or are related to some important goal to me.&lt;/p&gt;&lt;p&gt;I am still happy I build those side projects, I got a lot out of it:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Experience - knowing that you can do something and having feelings of competence are very important - they reduce the activation energy for starting new projects&lt;/li&gt;&lt;li&gt;Codebases - I can look up old solutions to common problems - having boilerplates is very valuable as it can make me go from idea to production faster&lt;/li&gt;&lt;li&gt;Blog posts - some distilled knowledge and signal of expertise&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Now without the small infrastructure burden and a slightly bigger psychological burden, I am freer to pick up new stuff. Wish me luck!&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Learning Deep Generative Modelling from Stanford</title>
       <link>https://tinystruggles.com/posts/stanford_deep_generative_modelling/</link>
       <pubDate>Fri, 30 Aug 2024 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/stanford_deep_generative_modelling/</guid>
       <description>&lt;p&gt;I am a big fan of self-directed learning, but I have never before spent almost 2 thousands of dollars on a single course. Was it worth it? Who could most benefit from that?&lt;/p&gt;&lt;p&gt;I took the &lt;a href=&#34;https://online.stanford.edu/courses/xcs236-deep-generative-models&#34;&gt;Deep Generative Modeling course from Standford online&lt;/a&gt; for 1750 dollars.&lt;/p&gt;&lt;p&gt;It was a first such course for me and before committing to it, I had some questions and doubts as it was rather expensive. I hope sharing my experience will help out people in a similar situation.&lt;/p&gt;&lt;p&gt;You can benefit from this post if you:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;are interested in learning about AI/ML or specifically generative modelling and you are wondering if &lt;a href=&#34;https://online.stanford.edu/courses/xcs236-deep-generative-models&#34;&gt;XCS236&lt;/a&gt; is a good course to take.&lt;/li&gt;&lt;li&gt;wonder if it’s worth to spend almost 2k usd on a course, vs 50 usd/free on other platforms/DIY approaches.&lt;/li&gt;&lt;li&gt;want to know how to best take advantage of such course if you were to invest in it.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;my-background&#34;&gt;My background&lt;/h2&gt;&lt;p&gt;Coming to the course I had been working in tech for 10+ years in software engineering positions and had a degree in Applied Physics. This meant that I was not afraid of math (calculus especially) and was very comfortable in coding. My math base was somewhat rusty, but I compensated by taking some Coursera courses to refresh the topics I needed more confidence in.&lt;/p&gt;&lt;p&gt;I also had some knowledge/experience with ML/AI - I took many courses along the years even though my job experience with that was very limited. I also have done some side projects that required using models.&lt;/p&gt;&lt;p&gt;My knowledge of generative modeling was mostly related to TTS models and content of &lt;a href=&#34;https://d2l.ai/&#34;&gt;Dive into Deep Learning&lt;/a&gt; and &lt;a href=&#34;https://www.youtube.com/playlist?list=PLqYmG7hTraZCDxZ44o4p3N5Anz3lLRVZF&#34;&gt;Deep Learning by Deep Mind and UCLA&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&#34;course-content-and-format&#34;&gt;Course content and format&lt;/h2&gt;&lt;p&gt;The course was focused on fundamental math and techniques in Generative Modeling:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Autoregressive models&lt;/li&gt;&lt;li&gt;Variational autoencoders&lt;/li&gt;&lt;li&gt;Normalizing flows&lt;/li&gt;&lt;li&gt;GANs&lt;/li&gt;&lt;li&gt;Energy based models&lt;/li&gt;&lt;li&gt;Score based models&lt;/li&gt;&lt;li&gt;Diffusion models&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;You can see the full &lt;a href=&#34;https://deepgenerativemodels.github.io/syllabus.html&#34;&gt;syllabus here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The course gave a deep understanding of probabilistic modeling across all the approaches and taught me a bunch of math tricks. The assignments helped me to better understand the math and as well as actually implement all the approaches on toy datasets.&lt;/p&gt;&lt;p&gt;The concepts were explained in a way that built on each other and gradually expanded the student&amp;rsquo;s arsenal of techniques.&lt;/p&gt;&lt;p&gt;The assignments were rather math heavy, but would really help with better understanding the nuance of the theory material which I later really appreciated. The coding parts were well done and focused on the core of the algorithms and math and provided good scaffolding for the essential bits to make things work, but not specific to the content of the course. The code was all in pytorch which is what is commonly used in the industry. The assignments focused on autoregressive models (including transformers), variational autoencoders and GANs. There weren’t any assignments related to diffusion modeling which was disappointing.&lt;/p&gt;&lt;p&gt;The course material was up to date on the latest diffusion based approaches. The main professor of the course &lt;a href=&#34;https://cs.stanford.edu/~ermon/&#34;&gt;Stefano Ermon&lt;/a&gt; is one of the top Generative Modelling researchers (H-index 84 as of 2024) with a specialty in diffusion modeling.&lt;/p&gt;&lt;p&gt;The course was not focused on transformers, LLMs, or language processing in general, if this is what you want to learn about, this is not the right course.&lt;/p&gt;&lt;h3 id=&#34;platform-and-format&#34;&gt;Platform and format&lt;/h3&gt;&lt;p&gt;The course was a mixture of pre recorded lectures broken into chunks on specific topics and a set of assignments that contained coding and written parts. It’s 10 weeks, but as it’s self paced it could be completed earlier.&lt;/p&gt;&lt;p&gt;Lectures were hosted on the stanford online platform (also available on youtube, but not unprocessed), the assignments were in private repositories on github, but you would submit the solutions on the stanford online platform.&lt;/p&gt;&lt;p&gt;There were 3 assignments and no graded projects. The assignments had pretty generous deadlines.&lt;/p&gt;&lt;p&gt;I believe that about 400 people participated in my course cohort. There was a community on slack for students on slack, and each student would also have a course facilitator, students would be encouraged to form groups on their own. There were regular live zoom meetings for office hours with the professor/TAs, show and tell and guest speakers as well. Course facilitators were available for 1:1 video chat or chats on slack.&lt;/p&gt;&lt;h2 id=&#34;what-i-enjoyed-in-the-course&#34;&gt;What I enjoyed in the course&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;Slack community for students and TAs/Course Facilitators&lt;/li&gt;&lt;li&gt;Study groups - where I could chat with other students and meet with them virtually&lt;/li&gt;&lt;li&gt;Additional resources for students&lt;/li&gt;&lt;li&gt;World class faculty - Stefano Ermon is one of the lead diffusion researchers!&lt;/li&gt;&lt;li&gt;Homeworks were difficult enough to motivate a lot of learning - lots of math proof1:1 time with course facilitators&lt;/li&gt;&lt;li&gt;Deep and well organized understanding of the models aided by theoretical homeworks and practical exercises&lt;/li&gt;&lt;li&gt;Well scaled down problems - no need for GPUs&lt;/li&gt;&lt;li&gt;Lectures also available on YouTube (convenient for watching while doing house chores :D)&lt;/li&gt;&lt;li&gt;Alumni slack and private linked-in group - this was a nice surprise at the end of the course. Based on the introductions on slack, there were very many cool professionals from all sorts of companies attending the course, so this is a goldmine of high quality contacts.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;money-on-the-line-helps-with-motivation&#34;&gt;Money on the line helps with motivation&lt;/h2&gt;&lt;p&gt;I start a lot of courses and I only finish some, it takes quite a lot of persistence and motivation to stick to them.&lt;/p&gt;&lt;p&gt;The fact that I spent 1750 dollars on the course made me really want to complete it. Just imagining that I could fail it and waste all this money would make my stomach churn. So I really put in the work. I prioritized time for the assignments, I started early and I studied additional resources.  It motivated me to revise my math skills and read additional papers.&lt;/p&gt;&lt;p&gt;The level of math in the first assignment was quite a shock to many of the students (including me!), but as I persisted I got into the groove of writing math proofs. My comfort level got expanded for sure.&lt;/p&gt;&lt;p&gt;That said, there was a window of time to give up on a course and get a refund or transfer to a different course if the content wasn’t matching the student’s expectation.&lt;/p&gt;&lt;h2 id=&#34;you-get-what-you-put-into-it&#34;&gt;You get what you put into it&lt;/h2&gt;&lt;p&gt;As the course was hard, just completing it provided a lot of value, because it required understanding of the topics. I completed the course with more than 100% of the points (thanks to assignments for extra credit!), but I still think that I could have taken more advantage of it!&lt;/p&gt;&lt;h3 id=&#34;what-did-i-do-right&#34;&gt;What did I do right?&lt;/h3&gt;&lt;p&gt;Some of the things I did right:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Learning from additional resources to fill in my gaps and enhance my understanding&lt;/li&gt;&lt;li&gt;Using Spaced Repetition (&lt;a href=&#34;https://apps.ankiweb.net/&#34;&gt;ANKI&lt;/a&gt;) to better retain the material - since I put all this effort, it would be a waste if I promptly forgot it&lt;/li&gt;&lt;li&gt;Choosing to do (most of) extra credit assignments&lt;/li&gt;&lt;li&gt;Meeting fellow students - I was a instigator of several discussions and video calls, people were pretty interesting&lt;/li&gt;&lt;li&gt;Contacting the course facilitator when stuck&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;what-should-i-have-done-differently&#34;&gt;What should I have done differently?&lt;/h3&gt;&lt;p&gt;I would have benefitted more if I did the following:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Picking a project to do before the course even starts and partnering with other students to get it done - this was the part that was different from the live stanford course - I think it would be provide a great excuse for a intense learning and collaboration and also taking better advantage of course resources (e.g. questions to professors and TAs)&lt;/li&gt;&lt;li&gt;Taking more advantage of the course facilitators - e.g. please double check my code or reasoning on a proof. It would save me from wasting time.&lt;/li&gt;&lt;li&gt;Collaboration with other students and taking advantage of networking&lt;/li&gt;&lt;li&gt;Take more advantage of live zoom sessions - I didn’t attend most of them and rewatched some of the recordings.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;That said, sometimes life gets in the way and it&amp;rsquo;s hard to maximize every opportunity, so I won&amp;rsquo;t be too hard on myself about it.&lt;/p&gt;&lt;h2 id=&#34;how-to-do-a-diy-alternative&#34;&gt;How to do a DIY alternative&lt;/h2&gt;&lt;p&gt;If you are a sufficiently motivated and well organized individual with lots of time on your hands, you could recreate most of the value of the course doing the following:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Watch the &lt;a href=&#34;https://www.youtube.com/playlist?list=PLoROMvodv4rPOWA-omMM6STXaWW4FvJT8&#34;&gt;youtube videos&lt;/a&gt; and read the lecture slides (&lt;a href=&#34;https://deepgenerativemodels.github.io/syllabus.html&#34;&gt;syllabus&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Read the associated materials&lt;/li&gt;&lt;li&gt;Implement generative modeling using major model approaches on toy datasets such as:&lt;ul&gt;&lt;li&gt;&lt;a href=&#34;https://www.kaggle.com/datasets/hojjatk/mnist-dataset&#34;&gt;MNIST&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;http://ufldl.stanford.edu/housenumbers/&#34;&gt;SVHN&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://www.kaggle.com/datasets/zalando-research/fashionmnist&#34;&gt;Fashion MNIST&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;Play with state of the art models from hugging face:&lt;ul&gt;&lt;li&gt;&lt;a href=&#34;https://huggingface.co/docs/transformers/en/index&#34;&gt;Transformers&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://huggingface.co/docs/diffusers/index&#34;&gt;Diffusion models&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;Devise and work on a project related to the topic&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;But what about the help of facilitators and the community?&lt;/p&gt;&lt;p&gt;There are ways to get around that too, for example:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Use discord, e.g. ML/AI communities, like one around &lt;a href=&#34;https://discord.com/invite/hugging-face-879548962464493619&#34;&gt;hugging face&lt;/a&gt; - to meet real people and get help.&lt;/li&gt;&lt;li&gt;Use your network to get skilled people to help you, you might actually know people who know a lot about AI/ML. People like to help smart and motivated individuals on the topics they are passionate about. If you don&amp;rsquo;t know such people, go to some local meetups.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;I found the course very valuable for me, but it’s not a course for everyone and you can also learn a lot on your own.&lt;/p&gt;&lt;p&gt;I was interested in the content and happy to put in the time, additionally my work sponsored 2/3rd of the cost of the course. High cost motivated me to prioritize it. And a Stanford credential was an added bonus.&lt;/p&gt;&lt;p&gt;But I would not immediately generalize the worth of that specific course for me to all such courses, even all the Stanford ones.&lt;/p&gt;&lt;p&gt;The factors I would look for to determine if the course if worth it for me:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;unique material that can’t be easily learned on other platforms&lt;/li&gt;&lt;li&gt;access to world class faculty&lt;/li&gt;&lt;li&gt;access to the alumni community/fellow students&lt;/li&gt;&lt;li&gt;topics relevant to my interests/future plans and my own availability to take advantage of the course&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I hope that helps! Stay curious!&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Postmortem of my 9 year journey at Google</title>
       <link>https://tinystruggles.com/posts/google_postmortem/</link>
       <pubDate>Fri, 16 Aug 2024 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/google_postmortem/</guid>
       <description>&lt;p&gt;I started writing this retrospective during my last week at Google, I have already wrapped up everything, had my goodbyes. In the spirit of SRE (as an ex-SRE), I thought it would be fun to write a little retrospective in the form of a &lt;a href=&#34;https://sre.google/sre-book/postmortem-culture/&#34;&gt;postmortem&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&#34;introduction&#34;&gt;Introduction&lt;/h2&gt;&lt;p&gt;I joined Google young and relatively inexperienced and had spent about 9 years there.&lt;/p&gt;&lt;p&gt;I started my journey in software at 19 (first internship) and then continued working part and full time while continuing my degree in Applied Physics. I got disillusioned with working in physics during the course of my degree, software turned out to be a more promising career path.&lt;/p&gt;&lt;p&gt;At some point I got head hunted by a Google sourcer. That resulted in an internship in London when I was 22, later I joined full time in Dublin. I worked in several teams around three products: Bigtable, Persistent Disk and GCE VMs (virtual machines). I include a detailed timeline at the bottom of this post.&lt;/p&gt;&lt;h2 id=&#34;what-did-i-expect-from-joining-google&#34;&gt;What did I expect from joining Google?&lt;/h2&gt;&lt;p&gt;After I got a taste of Google during my London internship, I was excited for more. What appealed to me was the level of engineering, how sophisticated and interesting the technology was and the level of engineers.&lt;/p&gt;&lt;p&gt;My only experience until then was with small software houses and startups in Poland, but Google… Google had some of the best tech in the world, and I had an opportunity to work with it and learn it. That was making me very excited.&lt;/p&gt;&lt;p&gt;Google impressed me. I didn&amp;rsquo;t see myself bored easily, I could stay in for longer, maybe even 5 years and I would still continue learning.Other things that appealed to me were the pay, the perks and fun international community.&lt;/p&gt;&lt;p&gt;From the big picture perspective, I wanted to start my own company, but a detour at Google could give me skills, fun and money to be better positioned to that in the future. I took the plunge.&lt;/p&gt;&lt;h2 id=&#34;so-how-was-it&#34;&gt;So, how was it?&lt;/h2&gt;&lt;p&gt;Overall, it was incredible. There were ups and downs. Heartwarming moments, joy of teamwork, satisfaction, as well as lots of stresses and frustrations.&lt;/p&gt;&lt;p&gt;It was a mutually beneficial exchange, I poured my energy, soul and cognitive resources and I got form it:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;tons of money&lt;/li&gt;&lt;li&gt;engineering skills, low and high level, great systems understanding and world class skills in handling crises and debugging&lt;/li&gt;&lt;li&gt;leadership and management skills&lt;/li&gt;&lt;li&gt;satisfaction from being part of a cool company&lt;/li&gt;&lt;li&gt;cool offsites and business travels&lt;/li&gt;&lt;li&gt;lifestyle perks, including things like pool in the office, world class gym, all sort of sport classes, weekly massages, tasty and/or healthy meals, healthcare on site&lt;/li&gt;&lt;li&gt;great community and relationships&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;At some point the exchange became less attractive to me. There were several factors:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;reached/exceeded my financial goals&lt;/li&gt;&lt;li&gt;fascination with Google tech waned:&lt;/li&gt;&lt;li&gt;industry somewhat caught up&lt;/li&gt;&lt;li&gt;reality of actual work not being that mind blowing&lt;/li&gt;&lt;li&gt;my interests saturated/shifted&lt;/li&gt;&lt;li&gt;desire to start my own company (yes, I can wait, but not for decades!)&lt;/li&gt;&lt;li&gt;company became less fun/cool&lt;/li&gt;&lt;li&gt;budget cuts (less business travel, no mind blowing offsites anymore)&lt;/li&gt;&lt;li&gt;layoffs&lt;/li&gt;&lt;li&gt;Head Count shifting to cheap locations aggressively - less opportunity for me to grow my organization locally&lt;/li&gt;&lt;li&gt;lots of overheads related to security/regulations&lt;/li&gt;&lt;li&gt;many hard engineering challenges due to complex systems and relationships between teams slowing the velocity&lt;/li&gt;&lt;li&gt;cognitive load - this was less of a factor in my last role, but bigger problem in the previous one. Google tech has lots of complexity and nuance, it’s pretty typical that an industry hire employee might need a year to fully ramp up which is pretty crazy&lt;/li&gt;&lt;li&gt;opportunities for personal growth somewhat unclear&lt;/li&gt;&lt;li&gt;there were no dramatic shifts in sight, just more of the same (which is also valuable, I just didn’t want coast)&lt;/li&gt;&lt;li&gt;tapped out career-wise: L6 ICs are pretty rare - it already is a top tier of seniority in engineering, I was not really interested in L7 as L7 is more of a political role than the engineering one on the individual track, and on the management track there was no headcount in sight… if I had an opportunity to become a manager of managers and run a big org under myself I would stay longer&lt;/li&gt;&lt;li&gt;shape of the technical work was not aligned with my interest in what I would like to be growing at.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;lessons-learned&#34;&gt;Lessons learned&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s hard to compress 9 years of learning into a bunch of bullet points. I learned technical skills, I learned soft skills, I became a better and a wiser person and a leader.&lt;/p&gt;&lt;p&gt;And in the spirit of being a (somewhat) anxious overachiever, I consistently felt that I could be doing better along the way - always motivating me to strive and learn.&lt;/p&gt;&lt;h3 id=&#34;what-went-well&#34;&gt;What went well&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;I got promoted quickly&lt;/li&gt;&lt;li&gt;I ended at L6 which is highly regarded and well compensated&lt;/li&gt;&lt;li&gt;I always had lots of autonomy&lt;/li&gt;&lt;li&gt;I could always push back for work life balance (reasonable hours and workload)&lt;/li&gt;&lt;li&gt;inspiring, motivated and smart coworkers&lt;/li&gt;&lt;li&gt;earn more money that I could have imagined&lt;/li&gt;&lt;li&gt;used a lot of perks that enabled very healthy lifestyle&lt;/li&gt;&lt;li&gt;lots of fun travels for work (business and offsites)&lt;/li&gt;&lt;li&gt;grew as an engineer and as a leader&lt;/li&gt;&lt;li&gt;drastically improved my soft skills&lt;/li&gt;&lt;li&gt;made a lot of work friends&lt;/li&gt;&lt;li&gt;learned about lots of cool technologies&lt;/li&gt;&lt;li&gt;had an opportunity to create my own team from scratch&lt;/li&gt;&lt;li&gt;working 60% or 80% were fantastic for my lifestyle and building relationships outside of work&lt;/li&gt;&lt;li&gt;learned a lot of tools for dealing with (chronic) stress&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;what-went-poorly&#34;&gt;What went poorly&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;I have overstayed as SRE - Dublin didn’t have much options and I didn’t move to a different site (inertia, personal reasons, etc)&lt;/li&gt;&lt;li&gt;oncall was stressing me out and disrupting my sleep,&lt;/li&gt;&lt;li&gt;not the best fit for my personality which is more optimistic and creative - this led to feeling of misalignment, I felt somewhat unfulfilled at - work in terms of type of work and projects and compensated outside of work with side projects&lt;/li&gt;&lt;li&gt;24/7 nature of the organization was making it hard to disconnect&lt;/li&gt;&lt;li&gt;USA centric culture, if you are not in USA at Google and don’t have a big presence in a location it’s a bit like swimming upstream, it’s easy to feel isolated, sidelined or on the flipside overwhelmed with late meetings&lt;/li&gt;&lt;li&gt;Promised HC not landing - e.g. I was promised further expansion twice, which later was scratched and then promised again in a bit different form…&lt;/li&gt;&lt;li&gt;senior level managers overwhelmed and not providing support/feedback or pretty much any oversight (there were moments when things felt a bit like a wild west)&lt;/li&gt;&lt;li&gt;there were many periods when I felt like I was overwhelmed with meetings/repetitive work/underresourced team, but at the same time not having good engineering/or management growth opportunities&lt;/li&gt;&lt;li&gt;cognitive load at Google is very high - there is a countless number of systems and technologies that it’s useful to have in mind and can affect your system in one way or another (this is especially bad in SRE)&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;where-did-i-get-lucky&#34;&gt;Where did I get lucky?&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;Google stock did very well, additionally with my rapid career trajectory, I did very well too 🙂&lt;/li&gt;&lt;li&gt;The people I hired turned out to be really solid&lt;/li&gt;&lt;li&gt;I somehow figured things out - building a track record of being high performer even though some things were definitely stretch opportunities&lt;/li&gt;&lt;li&gt;I made good financial decisions, could have been luckier, but the decisions had good thought process&lt;/li&gt;&lt;li&gt;I made amazing connections&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;what-could-i-have-done-differently&#34;&gt;What could I have done differently?&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;Move out of SRE earlier since from the beginning I knew it wasn’t what I wanted&lt;/li&gt;&lt;li&gt;Move to a different site - it’s easy to cling to a thing that is already good, there is a cost of switching as well, but I undervalued exploration historically&lt;/li&gt;&lt;li&gt;Take more advantage of education reimbursements (e.g. take more stanford online courses)&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;action-items&#34;&gt;Action items&lt;/h2&gt;&lt;p&gt;A typical postmortem would present a table of categorized action items. But I&amp;rsquo;m not here to mobilize anyone&amp;hellip;&lt;/p&gt;&lt;p&gt;The next thing for me is a sabbatical consisting of at least 6 months on exploring, relaxing, learning new things and expanding my idea of what I could be doing next.&lt;/p&gt;&lt;p&gt;I have a tendency to underexplore and I love being productive with a clear objective, so a sabbatical is a psychological challenge. I will be writing more on the topic so stay tuned!&lt;/p&gt;&lt;h2 id=&#34;timeline&#34;&gt;Timeline&lt;/h2&gt;&lt;h4 id=&#34;summer-2015---app-engine-sre-intern-london&#34;&gt;Summer 2015 - App Engine SRE Intern (London)&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;Cloud Bigtable SRE Era (Dublin)&lt;/li&gt;&lt;li&gt;joined as an L3 engineer&lt;/li&gt;&lt;li&gt;got promoted to L4 within 9 months&lt;/li&gt;&lt;li&gt;Cloud bigtable TL role&lt;/li&gt;&lt;li&gt;got promoted to L5 within 1.5 year&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&#34;persistent-disk-sre-era&#34;&gt;Persistent Disk SRE Era&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;joined as an L5&lt;/li&gt;&lt;li&gt;wondered around until landed in a work group related to project that later became hyperdisk&lt;/li&gt;&lt;li&gt;the US sister team disbanned and to be recreated in Seattle&lt;/li&gt;&lt;li&gt;became a defacto SRE TL of hyperdisk and created a team for it&lt;/li&gt;&lt;li&gt;promoted to L6&lt;/li&gt;&lt;li&gt;became a manager of Persitent Disk IO SRE&lt;/li&gt;&lt;li&gt;Persistent Disk SRE issues with burnout and attrition (SRE hiring issues, rapid dev org growth 40→200, etc)&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&#34;gce-fleet-maintenance-dev-era&#34;&gt;GCE Fleet Maintenance Dev Era&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;joined as an L6 IC SWE&lt;/li&gt;&lt;li&gt;director who hired me became a VP and dumped me on some other manager that was overloaded&lt;/li&gt;&lt;li&gt;took over a struggling internal project and made it successful and recruited a team around it in Dublin&lt;/li&gt;&lt;li&gt;transitioned from IC to a manager of 4 and then 6&lt;/li&gt;&lt;li&gt;started a 2nd team under a different senior manager&lt;/li&gt;&lt;li&gt;defragged the 2nd team because of the lack of head count, reinvented the main team charter&lt;/li&gt;&lt;/ul&gt;</description>
     </item>
   
     <item>
       <title>I quit my startup because of comic sans</title>
       <link>https://tinystruggles.com/posts/cofounder_breakup/</link>
       <pubDate>Fri, 14 Jul 2023 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/cofounder_breakup/</guid>
       <description>&lt;p&gt;Okay, it wasn’t really caused by the Comic Sans.&lt;/p&gt;&lt;p&gt;I had very fun 3 months of pretty fun collaboration and intense learning, but in the end I had this very clear feeling that I would be better of on my own.&lt;/p&gt;&lt;p&gt;It wasn’t awful, but it wasn’t great. There were many small and big things that were making our fit non-ideal, and I had doubts growing in me,and one Sunday night, a pick deck in Comic Sans topped me over the edge to call it quits.&lt;/p&gt;&lt;h2 id=&#34;company-vision-fit&#34;&gt;Company vision fit&lt;/h2&gt;&lt;p&gt;Both my partner and I wanted to build a profitable company that would be bigger than just the two of us. But he was extremely focused on getting investment, when I was more interested in retaining control and bootstrapping as much as possible.&lt;/p&gt;&lt;p&gt;It makes sense, I&amp;rsquo;m practically self sufficient - I have lots of money saved and invested, skills that could easily give me money from contract work and skills to build and (even) market the product.&lt;/p&gt;&lt;p&gt;On the other hand my cofounder was more dependent on others, had limited runway, higher monthly burn, and much weaker technical skillset. It made a ton of sense for him to be securing funding.&lt;/p&gt;&lt;p&gt;Funding company with venture investment can be pretty useful:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;there are some types of companies with high startup cost that are not feasible for a typical &amp;lsquo;bootstrapper&amp;rsquo;&lt;/li&gt;&lt;li&gt;acceleration of growth -  no one wants to go through a slog if you could make the growth faster&lt;/li&gt;&lt;li&gt;a blanket of security before you reach profitability&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;But it has a cost:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;You give away equity&lt;/li&gt;&lt;li&gt;You put time into securing funding instead of making a useful product, it takes a lot of time into making pitch decks, getting known around, etc.&lt;/li&gt;&lt;li&gt;it shapes the type of a business - ideally, it would be a &amp;lsquo;unicorn&amp;rsquo; and investors will push you into this direction even if it makes the business fail in the end&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I&amp;rsquo;m not sure what I want exactly, sometimes I think that I want a lifestyle business, sometimes I think I would like to make it big, but in either case I want it to be my decision and not due to pressure from investors.&lt;/p&gt;&lt;h2 id=&#34;work-style-fit&#34;&gt;Work style fit&lt;/h2&gt;&lt;p&gt;I found my cofounder work style to be chaotic. We would make a plan, he would pick a lot of items to do, but then everything would be in progress, but hardly anything really done unless there was an external deadline, like for an accelerator application. We would agree for a system for tracking tasks, but then he would not use it for the most of the time. Was that necessarily a deal breaker? Not really, people can have different styles andstill get work done. I wouldn&amp;rsquo;t force people who have many years of experience to change their work style just to suit me, but it also didn&amp;rsquo;t make me feel too good about it.&lt;/p&gt;&lt;p&gt;And there was also an aspect of attention to details. Which was not my cofounder strong suite and that was leading to a bit of frustration and rework on my side. I wanted to build a high quality product and that is at odds with low attention to detail.&lt;/p&gt;&lt;p&gt;The issues above don&amp;rsquo;t make my cofounder a bad entrepreneur, I think that a dynamic, slightly chaotic work style with lots of work in progress could work for some people, but I just found that I prefer people who are more methodical and steady.&lt;/p&gt;&lt;h2 id=&#34;skill-fit&#34;&gt;Skill fit&lt;/h2&gt;&lt;p&gt;I brought to the table very strong development skills, ability to execute quickly on the product. Thanks to my earlier product building adventures I also had plenty of ideas about idea validation and marketing and some experience with content marketing, SEO and paid advertising.&lt;/p&gt;&lt;p&gt;My cofounder brought the idea and the ability to talk to customers, many years of tech/consulting adjacent experience and &amp;ldquo;street fighting&amp;rdquo;. Ideally, he would also bring in marketing and design skills, but he was pretty green to marketing and not particularly design inclined.&lt;/p&gt;&lt;p&gt;With a non-technical/technical cofounders, the typical split would be to have a CEO and CTO, which would make me the CTO and give other important tasks to my partner.&lt;/p&gt;&lt;p&gt;However, what makes me interested in entrepreneurship is handling all those CEO things. I&amp;rsquo;m  actually interested in business. I wouldn&amp;rsquo;t want to delegate those aspects of the entrepreneurship. That combined with my partner being new to this, made me resistant to this partnership.&lt;/p&gt;&lt;h2 id=&#34;friendship-fit&#34;&gt;Friendship fit&lt;/h2&gt;&lt;p&gt;Turns out we had very, very little in common. Sure, we were focused mostly on working and the business, but during the time we were hanging out I haven&amp;rsquo;t discovered a single shared interest apart from wanting to start a business.&lt;/p&gt;&lt;p&gt;And even though we lived in the same city, it was quite far apart. And we had different lifestyles - he was a young father, living in the suburbs, me - tech person leaving in the downtown, doing many sports, etc.&lt;/p&gt;&lt;p&gt;On top of that, we had differences in the background. I&amp;rsquo;m pretty much a nerd: science, hacking, technology, fitness, finance. He was more of a businessy-consultant &amp;ldquo;people person&amp;rdquo;. And we would have differences of opinion based on that.&lt;/p&gt;&lt;h2 id=&#34;idea&#34;&gt;Idea?&lt;/h2&gt;&lt;p&gt;Was the idea a killer idea? The jury is still out, it was a very interesting project from the technology perspective, and I could see it working from the business perspective if we executed it very well. It wouldn&amp;rsquo;t be a particularly easy project, but it had potential.&lt;/p&gt;&lt;p&gt;But at the same time, I think I can do better in terms of difficulty of making it a successful business and skill fit for me. I don&amp;rsquo;t have a better alternative right now, but I will keep looking and there is always an option to start a boutique software solutions agency which to be honest also sounds like a lot of fun and an easier business to bootstrap.&lt;/p&gt;&lt;p&gt;It was pretty difficult to abandon it, I got really invested in building it, there were more things I wanted to finish, but it had high opportunity cost and working hard on it was wearing me out.&lt;/p&gt;&lt;h2 id=&#34;whats-next&#34;&gt;What&amp;rsquo;s next?&lt;/h2&gt;&lt;p&gt;After 3 months of intense learning and building while also working my day job I am going to be taking a bit of a break and instead work on non-critical side projects. I will also invest time into researching and validation of some new business ideas, but probably won&amp;rsquo;t jump full on into implementation.&lt;/p&gt;&lt;p&gt;Did I make a good decision? The time will tell, I think that it was right to go with my gut, because likely these many small problems and doubts would only get bigger with time.&lt;/p&gt;&lt;p&gt;Going on my own with the next project will for sure be hard and lonely. But I have the indie community and friends, it won&amp;rsquo;t be too bad!&lt;/p&gt;&lt;p&gt;Everything being just on me is a bit scary, but I did that in the past, I am very independent and I like to be in control. I got this! I&amp;rsquo;m not giving up on my entrepreneurial dream!&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Two worlds: idea people and developers</title>
       <link>https://tinystruggles.com/posts/two_worlds/</link>
       <pubDate>Sat, 29 Apr 2023 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/two_worlds/</guid>
       <description>&lt;p&gt;I recently attended a startup weekend and was surprised by the backgrounds of the other attendees.&lt;/p&gt;&lt;p&gt;As a software engineer myself, I expected to see mostly engineers, but that wasn&amp;rsquo;t the case. Instead, I found myself surrounded by people with a diverse range of backgrounds and experiences, all of whom shared one thing in common: they had an idea.&lt;/p&gt;&lt;p&gt;At the same time, couple months ago started running an Indie Hackers Meetup in Dublin, a software startup meetup that mostly attracts developers.&lt;/p&gt;&lt;p&gt;And from other breaking news, as a result of that startup weekend I&amp;rsquo;m now deep into a new project. For the first time joining forces with a cofounder. The project is still in pretty early stages and I don&amp;rsquo;t know if it will become a serious business yet. Fingers crossed.&lt;/p&gt;&lt;p&gt;This all got me thinking about the two distinct groups of people in the startup world - those with the skills to execute an idea but lacking a good business idea that would solve a real life problem and had financial potential, and those with great ideas but lacking the technical expertise to bring them to life.&lt;/p&gt;&lt;p&gt;How can they meet and should they join forces as cofounders?&lt;/p&gt;&lt;h2 id=&#34;two-groups-of-people&#34;&gt;Two groups of people&lt;/h2&gt;&lt;p&gt;Many developers dream of starting their own software businesses, while countless non-developers struggle to find the right development team to build their ideas.&lt;/p&gt;&lt;p&gt;Do they need to join forces to build a company together? Well, no. There are many other ways.&lt;/p&gt;&lt;p&gt;A developer can still get hired to work on another person’s idea and the dreamer with an idea can seek funding through an angel investment, family and friends or VC.&lt;/p&gt;&lt;p&gt;But the truth is, these approaches have some serious drawbacks.&lt;/p&gt;&lt;p&gt;For developers, it often means giving up control over the direction of the company they&amp;rsquo;re working for, in exchange for a relatively small amount of equity.&lt;/p&gt;&lt;p&gt;And for entrepreneurs, it can mean giving up a significant chunk of their ownership in order to secure funding. And even then, they may struggle to find the right talent to build the technology they need. In many cases, they end up working with developers who don&amp;rsquo;t share their vision or passion, resulting in subpar solutions and missed opportunities.&lt;/p&gt;&lt;p&gt;So, should they join forces directly then? In some cases, I believe yes.&lt;/p&gt;&lt;h2 id=&#34;benefits-of-bringing-the-idea-person-and-the-developer-together&#34;&gt;Benefits of bringing the idea person and the developer together&lt;/h2&gt;&lt;p&gt;For the developer, partnering with an idea person means they can work on a project they are passionate about, have more control over the direction of the company, and potentially own a larger percentage of the business. As a result, they&amp;rsquo;ll have a vested interest in the success of the company, and are more likely to go above and beyond to ensure its success.&lt;/p&gt;&lt;p&gt;For entrepreneurs, working with a skilled developer can help them build the technology they need to turn their idea into a reality, quickly and without breaking the bank. Instead of struggling to find the right talent, entrepreneurs can rely on a partner who shares their vision and passion, resulting in better solutions and increased opportunities for success. By partnering with a developer, entrepreneurs can also reduce their dependence on external funding, potentially allowing them to bootstrap their business and maintain a greater degree of control over their company&amp;rsquo;s direction and growth.&lt;/p&gt;&lt;p&gt;Through my indie hacking adventures, I realized that building a profitable product can be actually a very emotionally hard and lonely journey. Having a cofounder means that each person can support the other through the ups and downs of building a business. They can share the workload and take on different responsibilities, leveraging each other&amp;rsquo;s strengths to achieve success. This mutual support is crucial when it comes to staying motivated and pushing through the difficult times that inevitably arise in any startup journey.&lt;/p&gt;&lt;p&gt;This all sounds pretty great. But are ‘just an idea’ or ‘just dev skills’ enough to partner up? I think that partnering up with anyone shouldn’t be considered lightly.&lt;/p&gt;&lt;h2 id=&#34;more-than-just-an-idea&#34;&gt;More than just an idea&lt;/h2&gt;&lt;p&gt;While having a great idea is a good starting point, it&amp;rsquo;s not enough to make someone a valuable cofounder. Yes, ideas are important, but I believe that for startups, the real value lies in the execution and ability to iterate.&lt;/p&gt;&lt;p&gt;Think about it - the likelihood that the first version of the idea will work is incredibly small. It will likely require a lot of iteration and pivoting, and the end result may be something completely different than what you originally envisioned.&lt;/p&gt;&lt;p&gt;Don’t bring someone as a cofounder if they just contribute an idea, but don’t have much else to offer.&lt;/p&gt;&lt;h2 id=&#34;more-than-just-development-skills&#34;&gt;More than just development skills&lt;/h2&gt;&lt;p&gt;Similarly, bringing up just skilled development labor is also not enough to make someone a valuable cofounder.&lt;/p&gt;&lt;p&gt;Some companies don&amp;rsquo;t require extensive development work. And even if technology is critical to your business, simply having a skilled developer on board is not enough.&lt;/p&gt;&lt;p&gt;Early stage business often require a lot of non-coding work. And if the business succeeds, the startup will need a CTO, not just a developer and that is a very different role requiring different types of skills.&lt;/p&gt;&lt;h2 id=&#34;cofounding-is-a-long-term-commitment&#34;&gt;Cofounding is a long term commitment&lt;/h2&gt;&lt;p&gt;Even if you bring considerably more than just idea or just the development, and are both excited about the project, before you commit to such a partnership, it&amp;rsquo;s important to consider the long-term implications.&lt;/p&gt;&lt;p&gt;As a co-founder, you&amp;rsquo;ll be investing time, effort, and potentially money into the venture. You&amp;rsquo;ll need to share the vision and values of the idea-non-tech co-founder, and be willing to work together to build the company, not only when times are great, but especially in crisis, when things are not working.&lt;/p&gt;&lt;p&gt;A successful startup requires a diverse set of skills, such as business strategy, marketing, and customer development. You and your co-founder should complement each other&amp;rsquo;s strengths and weaknesses, and have a clear understanding of your roles and responsibilities and over time develop strong mutual trust.&lt;/p&gt;&lt;p&gt;If you are not sure that your potential co-founder is ready for that sort of commitment, don’t become co-founders. Or start with a limited time mini-project first.&lt;/p&gt;&lt;p&gt;There are other ways to take advantage of the complementary skills.&lt;/p&gt;&lt;h2 id=&#34;taking-advantage-of-the-other-camp-without-co-founding&#34;&gt;Taking advantage of the other camp without co-founding&lt;/h2&gt;&lt;p&gt;Not everyone is ready to commit to a long-term partnership, and that&amp;rsquo;s okay.&lt;/p&gt;&lt;p&gt;There are other ways to collaborate and take advantage of each other&amp;rsquo;s expertise without co-founding.&lt;/p&gt;&lt;p&gt;As a developer, you can go to startup events, meet idea people and collect a wide range of ideas there.&lt;/p&gt;&lt;p&gt;And as an idea person, you can approach developers to get their input on your ideas and even hire freelancers to build a minimum viable product. This way, you can still benefit from the knowledge of both camps without the burden of co-founding.&lt;/p&gt;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;Overall, joining forces can be magical. With the right partner by your side, you can achieve more than you ever could alone, and build a company that you&amp;rsquo;re proud of.&lt;/p&gt;&lt;p&gt;But it’s also a big potential risk to partner with anyone as cofounders and there are other ways to utilize the complementary skillsets and perspectives.&lt;/p&gt;&lt;p&gt;Regardless if you want to commit or not, I think it’s worth to explore the other camp. Go to developer events as a business/idea person and go to business events as a developer.&lt;/p&gt;&lt;p&gt;I strongly believe we could benefit from more cross-pollination.&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Fun indie projects</title>
       <link>https://tinystruggles.com/posts/fun_indie_projects/</link>
       <pubDate>Mon, 27 Feb 2023 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/fun_indie_projects/</guid>
       <description>&lt;p&gt;I wrote in November that I&amp;rsquo;m taking a &lt;a href=&#34;https://tinystruggles.com/posts/abandoning_indie_hacking/&#34;&gt;break from indie hacking&lt;/a&gt;, well, the break didn&amp;rsquo;t take long (2-3 months).&lt;/p&gt;&lt;p&gt;A break made sense at that time, because my project &lt;a href=&#34;https://watchlimits.com&#34;&gt;watchlimits.com&lt;/a&gt; wasn&amp;rsquo;t getting traction and I just changed jobs. New job presented a lot of new learning opportunities and as any change was somewhat stressful, so I didn&amp;rsquo;t want to overload myself and risk a burnout.&lt;/p&gt;&lt;p&gt;I am back in a not fully &amp;ldquo;serious&amp;rdquo; capacity. I am not doing any serious business right now. I have a bunch of promising ideas, but for now I&amp;rsquo;m exploring some pretty dumb ones.&lt;/p&gt;&lt;p&gt;I&amp;rsquo;m working on projects that I don&amp;rsquo;t plan to monetize and for which the main goals are having creative fun and learning.&lt;/p&gt;&lt;h2 id=&#34;fun-and-learning--mrr&#34;&gt;Fun and learning &amp;raquo; MRR&lt;/h2&gt;&lt;p&gt;People have different motivations.&lt;/p&gt;&lt;p&gt;Often it&amp;rsquo;s about making money. Money can be a great motivator or a bad one. It&amp;rsquo;s a great one when it&amp;rsquo;s aligned with making something actually valuable for others. But it&amp;rsquo;s a bad one if you feel like it forces you to make the wrong choices.&lt;/p&gt;&lt;p&gt;There are many valuable things in life, we should focus on the ones most important to us in the moment. I have a full time job, which pays me well, but constrains me in terms of what I do and how I do it.&lt;/p&gt;&lt;p&gt;Being independent allows me to do things that don&amp;rsquo;t earn money, just because I want to do them.&lt;/p&gt;&lt;p&gt;So right now I am focusing on having fun and learning new things through my side projects. And I am serious about this. Fun and learning are the main goals, a little challenge or discomfort is fine, but no sustained grind.&lt;/p&gt;&lt;p&gt;My current priorities:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;being creative &amp;amp; playful&lt;/li&gt;&lt;li&gt;building my craft skills - variety of stuff I can build and speed of prototyping&lt;/li&gt;&lt;li&gt;shipping &amp;amp; sharing stuff&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I would like to be able to do it forever. I think that the person who has the most fun wins and is least likely to burn out and give up.&lt;/p&gt;&lt;p&gt;It doesn&amp;rsquo;t mean that I will on purpose not try to make any money. Making money can be fun!&lt;/p&gt;&lt;p&gt;But I&amp;rsquo;m also ok with doing projects that have no monetary prospects at all and are just a fun, creative expression.&lt;/p&gt;&lt;h2 id=&#34;new-projects&#34;&gt;New projects&lt;/h2&gt;&lt;p&gt;Expect new projects to spring up like mushrooms 🍄🍄🍄.&lt;/p&gt;&lt;p&gt;I&amp;rsquo;m ok to start a lot of things and to also abandon a lot of things if they don&amp;rsquo;t spark joy anymore.&lt;/p&gt;&lt;h3 id=&#34;-focus-turtle&#34;&gt;🐢 Focus Turtle&lt;/h3&gt;&lt;p&gt;&lt;a href=&#34;https://focusturtle.com&#34;&gt;Focus Turtle&lt;/a&gt; is a chrome extension that helps people focus by reminding them about their goal when they get distracted.&lt;/p&gt;&lt;p&gt;Stack:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;React&lt;/li&gt;&lt;li&gt;TypeScript&lt;/li&gt;&lt;li&gt;Tailwind CSS&lt;/li&gt;&lt;li&gt;Vercel&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Goals:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;do something cute and fun - creative outlet&lt;/li&gt;&lt;li&gt;experiment more with the browser extensions&lt;/li&gt;&lt;li&gt;get better at design, react, typescript, ux&lt;/li&gt;&lt;li&gt;another digital asset that brings people into my orbit of my project ecosystem&lt;/li&gt;&lt;li&gt;make something useful for other people&lt;/li&gt;&lt;li&gt;have fun!&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Non goals:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;making the focus turtle into a commercial product&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;With these goals in mind:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;I can spend time to do things right, there is no rush&lt;/li&gt;&lt;li&gt;It&amp;rsquo;s ok to waste time being &amp;ldquo;inefficient&amp;rdquo; - I&amp;rsquo;m really bad at something it means I am outside of my skill comfort zone&lt;/li&gt;&lt;li&gt;Things that push me into new directions / are a practical application of my skills win&lt;/li&gt;&lt;li&gt;Don&amp;rsquo;t do things that feel like a grind&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;-beat-focus&#34;&gt;💓 Beat Focus&lt;/h3&gt;&lt;p&gt;&lt;a href=&#34;https://play.google.com/store/apps/details?id=com.justynailczuk.beatfocus&amp;amp;hl=en-US&amp;amp;ah=Hz25npOqK8CQcT9PKQYIEc8ldaQ&#34;&gt;Beat Focus&lt;/a&gt; is a mobile app that let&amp;rsquo;s user play binaurial beats on a timer in a background when they need an extra focus boost.&lt;/p&gt;&lt;p&gt;Stack:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;React Native&lt;/li&gt;&lt;li&gt;TypesScript&lt;/li&gt;&lt;li&gt;Expo&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Goals:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;I wanted an app like this as I like focusing for short bursts of time and would listen to binaural beats myself&lt;/li&gt;&lt;li&gt;learn to develop apps with React Native&lt;/li&gt;&lt;li&gt;Make something quick and simple but extensible&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;-aer-teach&#34;&gt;🏠 Aer Teach&lt;/h3&gt;&lt;p&gt;Yet another Chrome Extension! This one was a group project developed during &lt;a href=&#34;https://www.linkedin.com/posts/smart-dublin_thats-a-wrap-thanks-to-the-judges-volunteers-activity-7035989653199450112-bjp4?utm_source=share&amp;amp;utm_medium=member_desktop&#34;&gt;Dublin Air Quality Data Hack&lt;/a&gt; hackathon in late Feb 2023.&lt;/p&gt;&lt;p&gt;The idea there is to let you see pollution data right when you are looking for a place to buy or rent based on extensive new air quality data from Air Street View dataset.&lt;/p&gt;&lt;p&gt;So the extension is integrated with Daft.ie, the biggest real estate website in Ireland, the extension will display the relevant info, link to additional places to learn more every time user browses the listings. This is useful, becausepeople might remember once to check the data about air quality, but forget the next time as they likely won&amp;rsquo;t have a habit. On top of it, it saves a lot of otherwise tedious work.&lt;/p&gt;&lt;p&gt;The pollution data is processed by a python backend behind a simple API.&lt;/p&gt;&lt;p&gt;We haven&amp;rsquo;t finished everything we wanted so it&amp;rsquo;s not published yet, but stay tuned.&lt;/p&gt;&lt;h3 id=&#34;indie-hackers-dublin&#34;&gt;Indie Hackers Dublin&lt;/h3&gt;&lt;p&gt;So&amp;hellip; I would like Dublin to have a more thriving Indie Tech Scene. I think that if the change you want doesn&amp;rsquo;t happen you can always roll up your sleeves yourself.&lt;/p&gt;&lt;p&gt;So I&amp;rsquo;m organizing a new community via MeetUp: &lt;a href=&#34;https://www.meetup.com/indie-hackers-dublin/&#34;&gt;https://www.meetup.com/indie-hackers-dublin/&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I&amp;rsquo;m excited about the first meetup that is happening later this week. I have 16 people who said yes and no real experience running meetup communities.&lt;/p&gt;&lt;p&gt;But hey, I&amp;rsquo;m a capable individual with lots of skills, I&amp;rsquo;ll figure it out.&lt;/p&gt;&lt;h3 id=&#34;hackathons&#34;&gt;Hackathons&lt;/h3&gt;&lt;p&gt;So far this year (February), I have done 2 hackathons, one was a casual at my own place, the second one was the &lt;a href=&#34;https://www.linkedin.com/posts/smart-dublin_thats-a-wrap-thanks-to-the-judges-volunteers-activity-7035989653199450112-bjp4?utm_source=share&amp;amp;utm_medium=member_desktop&#34;&gt;Dublin Air Quality Data Hack&lt;/a&gt; and I&amp;rsquo;m signed up for another one: &lt;a href=&#34;https://www.eventbrite.ie/e/techstars-startup-weekend-women-dublin-tickets-484243233547?keep_tld=1&#34;&gt;Techstars Startup Weekend Women Dublin&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;It is pretty thrilling, even though pretty exhausting.&lt;/p&gt;&lt;p&gt;My experience so far was great, it feels like I&amp;rsquo;m back in my early twenties.&lt;/p&gt;&lt;h3 id=&#34;not-yet-started&#34;&gt;Not yet started&lt;/h3&gt;&lt;p&gt;And there is so much more&amp;hellip;&lt;/p&gt;&lt;p&gt;Let&amp;rsquo;s give you a taste of domains I have bought recently:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;myworkoutlist.com&lt;/li&gt;&lt;li&gt;mobilitytutor.com&lt;/li&gt;&lt;li&gt;healthyelbows.com&lt;/li&gt;&lt;li&gt;myweblimits.com &amp;lt;- this is for a new pivot of &lt;a href=&#34;https://watchlimits.com&#34;&gt;watchlimits.com&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I also still have studytimer.org with nothing on it!&lt;/p&gt;&lt;h2 id=&#34;summary&#34;&gt;Summary&lt;/h2&gt;&lt;p&gt;I think it&amp;rsquo;s important to know what game you are playing. Sometimes it&amp;rsquo;s scaling your business and persisting through setbacks. Sometimes it&amp;rsquo;s fun &amp;amp; exploration.&lt;/p&gt;&lt;p&gt;For now I will focus on having the most fun I can while keeping it sustainable and avoiding grinding myself down. So there will be a lot of things, but progress on any individual thing might be slow. I will also take days off work to compensate for intense hackathon orside project weekends to keep my overall load reasonable.&lt;/p&gt;&lt;p&gt;This choice has its tradeoffs. It&amp;rsquo;s the opposite of focusing and building momentum on a single project. I will do a lot of crappy things, I won&amp;rsquo;t have time to finish them off properly.&lt;/p&gt;&lt;p&gt;I expect that this free exploration phase will expand my horizons and will make me better at prototyping and getting things done (I will also have lots of code to copy paste from ;) )!&lt;/p&gt;&lt;p&gt;And if I find something very promising, I might switch back to the exploit mode and focus for real.&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>I left my Site Reliability job</title>
       <link>https://tinystruggles.com/posts/leaving_sre/</link>
       <pubDate>Sun, 27 Nov 2022 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/leaving_sre/</guid>
       <description>&lt;p&gt;I left my Site Reliability Engineering job after almost seven years.I didn&amp;rsquo;t leave Google though, just changed to another role within Software Engineering in the Cloud Infrastructure.&lt;/p&gt;&lt;p&gt;I was somewhat unhappy with my cushy role as a Site Reliability Engineering (SRE) Manager at Google. After quite a lot of deliberation, exploring opportunities and some setbacks (hello, industry layoffs and hiring freezes), I left my SRE job. My last day was October 14th 2022.&lt;/p&gt;&lt;p&gt;I was in SRE for almost 7 years even though I never actively planned SRE to be my career.&lt;/p&gt;&lt;h2 id=&#34;how-i-ended-up-as-an-sre&#34;&gt;How I ended up as an SRE&lt;/h2&gt;&lt;p&gt;Let me say this, even though I have worked in Backend Development positions pre-Google, I had interests that would make me a good SRE candidate.&lt;/p&gt;&lt;p&gt;In 2014/2015 I learned about DevOps. I set up all automation and deployment in the startup I worked at the time and I got fascinated by containers and all things cloud infrastructure.&lt;/p&gt;&lt;p&gt;I wrote multiple blog posts about Docker, gave workshops, organized meetups and even had a book deal (seriously! They give book deals to kids like me at the time!). I also was surrounded by various hardware hackers and serious sysadmins in the Warsaw Hackerspace. Hackerspace was my second home, and I would spend 20+ hours per week there hanging out with other hacker friends, building various software and hardware projects.&lt;/p&gt;&lt;p&gt;Even though I was very successful as a Dev in Poland, when Google offered me an internship abroad, I jumped at it. Being 22, it was a great opportunity to get out of my comfort zone. I guess because of my DevOps/Infra interests I got slotted into a SRE internship.&lt;/p&gt;&lt;p&gt;SRE internships are a bit weird, you basically end up being a Dev within a SRE team. It went pretty well and I got excited about the opportunities, the tech was so cool, people were really smart and inspiring. I wanted to try working for Google long term.&lt;/p&gt;&lt;p&gt;And I got it. Even though Dublin was not my first preference I accepted the role and ended up in Dublin as a SRE.I mentioned to the recruiter that I would prefer the SWE position, but somehow “it wasn’t possible”. Well, I was young and didn&amp;rsquo;t push for what I wanted hard enough. I knew that coming from a SRE internship it was a default path and was just happy about the opportunity in the end.&lt;/p&gt;&lt;h2 id=&#34;inertia-and-sre-only-site&#34;&gt;Inertia and SRE only site&lt;/h2&gt;&lt;p&gt;Later on I learned that Google Dublin was an “SRE only site”. All the eng teams were in SRE.&lt;/p&gt;&lt;p&gt;I uprooted my life to move to Dublin, which was disruptive on its own. But later I also added a lot of inertia by:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;accumulating significant investments&lt;/li&gt;&lt;li&gt;buying an apartment (1 &amp;amp; 2)&lt;/li&gt;&lt;li&gt;getting married here (to an American - creating a international couple leaving abroad)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;All of these would make a move quite a hassle.&lt;/p&gt;&lt;p&gt;Ireland is not the best country in the universe, but it&amp;rsquo;s pretty damn decent. It has a lot going for it that is aligned with what I value and enjoy.&lt;/p&gt;&lt;p&gt;Well, why not leave Google and get a better suiting job in a different company?&lt;/p&gt;&lt;p&gt;Here are some reasons adding to the inertia:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;To interview you need to have enough mental space and practice interviewing.&lt;/li&gt;&lt;li&gt;Google pays very well compared to other companies in Dublin, offers great work life balance and perks (free food, subsidized massages, etc)&lt;/li&gt;&lt;li&gt;After a while my outside of Google options would be SRE or management roles because this is what I was doing currently and had experience in - which if I was to do, I would rather do at Google&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It was much easier to just continue and make the most out of it, instead of making a big jump into unknown.&lt;/p&gt;&lt;h2 id=&#34;making-the-most-of-it&#34;&gt;Making the most of it&lt;/h2&gt;&lt;p&gt;Even though that I would have preferred to be a SWE instead of SRE, the job gave me an opportunity to work on very interesting engineering problems and codebases with some amazing engineers.&lt;/p&gt;&lt;p&gt;I learned how to do &amp;ldquo;good enough job&amp;rdquo; operationally and focused on making impact in a way it was expected from an SRE.&lt;/p&gt;&lt;p&gt;In infrastructure products, most of the difficult, design and code heavy engineering falls onto the dev teams, not the SRE teams.In some ways, SREs shield the dev teams from disruptions and cognitive load to make progress on cranking out features.  However, SRE work can still be pretty interesting and I was set on making the most out of it.&lt;/p&gt;&lt;p&gt;I pretty quickly gravitated to leadership roles. Being given trust and autonomy makes any job easier. I quickly realized that many of the realities that we take for granted are just a current setup, which is not the only possible one. The rules and dynamics can be changed. Vision and direction will often stem up from the personality and opinions of the given leader or set of leaders.&lt;/p&gt;&lt;p&gt;I was on a roll, I was productive, I got promoted pretty quickly (L3 -&amp;gt; L4 in 9 months, L4 -&amp;gt; L5 (senior) in 1.5 year, L5 -&amp;gt; L6 (staff) in 3 years).&lt;/p&gt;&lt;p&gt;When things become somewhat unbearable, e.g. I got bored of the type of the work I was doing and types of problems available, I would switch things up. Being promoted helped with this as I was given bigger and bigger opportunities.&lt;/p&gt;&lt;p&gt;I made 3 job changes before I left SRE, Individual Contributor (IC) -&amp;gt; Tech Lead (TL) in my first team, first team to the second team and then in my second team from IC to Manager.&lt;/p&gt;&lt;p&gt;Each time the change energized me and triggered a lot of growth. Changing teams helped a lot. Leaning into leadership empowered me and taught me a lot even though it usually was more stressful than IC roles.&lt;/p&gt;&lt;h2 id=&#34;skill-growth&#34;&gt;Skill growth&lt;/h2&gt;&lt;p&gt;Growing skills is important for building confidence and increasing your value to the team, but also it can be satisfying by itself.&lt;/p&gt;&lt;p&gt;I derive a deep satisfaction from learning. And ideally, I want to keep my personal skill interests and job interests aligned as much as possible.&lt;/p&gt;&lt;p&gt;And I did for a while, learning more about distributed systems, databases, reliability was pretty interesting. (At least for a while, but it’s applicability to my day to day job was limited).&lt;/p&gt;&lt;p&gt;When I decided to switch to management, it unlocked a lot of new learning. It got me out of my comfort zone and forced me to become more mature.&lt;/p&gt;&lt;p&gt;I experienced new organizational dynamics and people dynamics. I had attrition, I hired, I trained, I managed performance, I motivated, I coached.&lt;/p&gt;&lt;p&gt;It pushed me to learn more about myself and other people. I researched books, I read and read, and read. I learned a lot of soft skills and how to be more resilient. But I think I reached a point of being pretty good at my job in that specific environment.&lt;/p&gt;&lt;p&gt;At the same time, my technical learning at the job slowed a lot and the opportunities for learning were not that exciting to me.&lt;/p&gt;&lt;p&gt;Overall, I’ve never been passionate about being very good at troubleshooting, or managing incidents or crises. Probably I’ve been more interested in that than a typical Dev, but comparing to some of the best people I knew, the difference of interest was clear.&lt;/p&gt;&lt;p&gt;In the meantime, I’ve been doing quite a lot of indie hacking and building products from scratch. This also triggered desire to learn, but in many ways it wasn&amp;rsquo;t aligned with what would help me get ahead at work.&lt;/p&gt;&lt;p&gt;It caused me feelings not having enough time to learn all the things I wanted to learn as my interests weren&amp;rsquo;t aligned.&lt;/p&gt;&lt;h2 id=&#34;stress-overload-organizational-tensions-and-multi-year-projects&#34;&gt;Stress, overload, organizational tensions and multi-year projects&lt;/h2&gt;&lt;p&gt;I didn&amp;rsquo;t change jobs because of burnout, but I had moments of burning out before I made the decision to switch.&lt;/p&gt;&lt;p&gt;There was stress, overload and issues. This winter I had some work drama with a poor performer, there were problems with hiring in Dublin and some pushy managers of other teams.On top of it I was training very hard and working on side hustles. It didn&amp;rsquo;t feel to me as if I was working on my job that hard,but the stress was amplified by me putting myself under other demands and also a fundamental feeling of misalignment.&lt;/p&gt;&lt;p&gt;Side projects made it very clear to me that I need creative outlets, that I want to build my own company, create things from scratch. Meanwhile I had to go to another meeting, write performance reviews and do another oncall shift.&lt;/p&gt;&lt;p&gt;Dev and SRE are supposed to be partners, but often have a tense relationship. In a typical product area, Devs are leading the org shipping new features, making existing infra better and the SRE are exposed to risk and toil coming from these changes. SREs have their own projects, of course, but need to juggle lots of demands and dooming risks.&lt;/p&gt;&lt;p&gt;If you ever heard about circles of concern and influence, a typical SRE TL or manager would have a very wide circle of concern, wide circle of influence and tiny circle of control (due to limited staffing and other commitments). Of course influencing works, but it&amp;rsquo;s not free and has significant time and cognitive cost. I didn&amp;rsquo;t enjoy this dynamic too much.&lt;/p&gt;&lt;p&gt;Additionally, I felt stuck with a very long term project. It&amp;rsquo;s been multiple years in the making, I impacted it a lot and was deep into the weeds. It would keep evolving for many years to come, but wouldn&amp;rsquo;t be fully done for many years. It was still exciting in many aspects, but the rate of learning has decreased significantly. I felt like it would be pretty hard for me to get untangled from it.&lt;/p&gt;&lt;p&gt;Overall, I realized that this job was making me unhappy and unsatisfied.&lt;/p&gt;&lt;p&gt;I had a lot of decent days, some amazing days, and some “oh crap, I don’t want to do this anymore” days. It’s possible that my unhappy days would just be unhappy and I would find a reason to justify it. Human minds and emotions are not to be fully trusted! It’s also possible that things like my side hustle being a crappy business contributed to some negative emotions.&lt;/p&gt;&lt;h2 id=&#34;finding-alignment--imagining-the-next-role&#34;&gt;Finding alignment &amp;amp; imagining the next role&lt;/h2&gt;&lt;p&gt;I decided that I will change roles back in May and planned to end my SRE role at the end of September. The team wasn&amp;rsquo;t in the best place, there were projects I wanted to leave in a good place and I wanted to explore the available options.&lt;/p&gt;&lt;p&gt;I wasn&amp;rsquo;t exactly sure what I wanted. I knew what I didn&amp;rsquo;t want - management and SRE.&lt;/p&gt;&lt;p&gt;Pandemic opened more opportunities and people were more open to hiring remotely which was quite exciting.&lt;/p&gt;&lt;p&gt;I dated a bunch of different opportunities. Would project management be too big of a step? Maybe Chrome, it would be so different from SRE. DevRel could give me some useful experience building products intended for developers.&lt;/p&gt;&lt;p&gt;I even bought a book on finding the &lt;a href=&#34;https://www.goodreads.com/en/book/show/34331071-a-job-to-love&#34;&gt;job to love&lt;/a&gt; (which I still haven&amp;rsquo;t finished!).&lt;/p&gt;&lt;p&gt;I wanted a job that would be better aligned with my long term goals (entrepreneurship and building stuff) or generally a low demand job that would let me focus on those other goals in my free time.&lt;/p&gt;&lt;p&gt;My self imposed deadline was getting closer and I still haven&amp;rsquo;t found the right job when the internal hiring freeze hit.&lt;/p&gt;&lt;p&gt;Let&amp;rsquo;s say that it messed up some of my plans. But in the end I ended up with a role that was both a great match for my existing experience and for what I did next. So I guess the long search time paid off?&lt;/p&gt;&lt;h2 id=&#34;leaving-good-things-behind&#34;&gt;Leaving good things behind&lt;/h2&gt;&lt;p&gt;Even though, the job wasn&amp;rsquo;t aligned with my long term goals and was making me unhappy, it was hard to change.&lt;/p&gt;&lt;p&gt;As a manager I had my team. I developed pretty deep relationships with people who I managed and also had a great relationship with my own manager and with many other partners across the organization.&lt;/p&gt;&lt;p&gt;The problems with overload also drastically improved over last couple months and in general problems were being addressed.&lt;/p&gt;&lt;p&gt;I was also good at my job and could make solid impact with relatively little effort gliding on my existing knowledge and influence.&lt;/p&gt;&lt;p&gt;It wasn&amp;rsquo;t terrible. Was I falling a victim of &amp;ldquo;grass is always greener&amp;rdquo;? Could the new role be significantly worse?&lt;/p&gt;&lt;h2 id=&#34;what-next&#34;&gt;What next&lt;/h2&gt;&lt;p&gt;Directionally, I still want to build a company at some point. I want to grow as a leader. But I also want to grow as an engineer and be a very skilled individual who understands math and science.It&amp;rsquo;s hard to get all those things at once. For now I will focus on my love for engineering.&lt;/p&gt;&lt;p&gt;Should I put off building a company for another couple years if I wanted to do it since I wanted to be a teenager?Through indie-hacking learned that building a product that people want is much more than just building software.&lt;/p&gt;&lt;p&gt;Could I just do it now? Yes. I think that eventually I would succeed.&lt;/p&gt;&lt;h3 id=&#34;working-on-indie-products-made-me-appreciate-employment-more&#34;&gt;Working on indie products made me appreciate employment more&lt;/h3&gt;&lt;p&gt;Failing to find significant traction with watchlimits (my indie product) made me appreciate how wonderful paid employment can be.&lt;/p&gt;&lt;p&gt;You get paid a lot, you are expected to be learning, the work life balance is good, you have support network and you work in a space where there is already a lot of traction and your work is almost guaranteed to have impact.&lt;/p&gt;&lt;p&gt;So, since I found a role that is better aligned with what I want long term and has the best what paid employment can offer I will try to make most of it.&lt;/p&gt;&lt;h3 id=&#34;i-will-be-working-for-learning-and-fun-from-now-on&#34;&gt;I will be working for learning and fun from now on&lt;/h3&gt;&lt;p&gt;I am financially in a very good place. I don&amp;rsquo;t have enough to never have to work again in my life, but I&amp;rsquo;m not that far off. And I am confident that I will earn money as I enjoy working for the most part.&lt;/p&gt;&lt;p&gt;From now on, my guiding principle for work is that I will try to make my job so good that I would want to work there for free. Main reasons for working being: learning, fun and satisfaction of making positive impact.&lt;/p&gt;&lt;p&gt;I ended up picking up the most intense role available to me, challenging on many levels, but I have confidence that I can pull it off without sacrificing my sanity or health. I&amp;rsquo;m in my prime years and ready for a challenge.&lt;/p&gt;&lt;p&gt;The future is pretty exciting.&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Abandoning the indie hacking dream for now</title>
       <link>https://tinystruggles.com/posts/abandoning_indie_hacking/</link>
       <pubDate>Fri, 11 Nov 2022 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/abandoning_indie_hacking/</guid>
       <description>&lt;p&gt;I’m taking a break from indie-hacking&lt;/p&gt;&lt;p&gt;Will I never write a line of indie code again? Obviously not. I love building, sharing and learning.&lt;/p&gt;&lt;p&gt;But I am going to stop trying to make profitable indie-businesses for the next couple months or a year.&lt;/p&gt;&lt;p&gt;Building things is fun, but marketing a business that has no traction is a grind and a roller coaster. Something that right now I am tired of.&lt;/p&gt;&lt;h2 id=&#34;no-need-for-a-profitable-business&#34;&gt;No need for a profitable business&lt;/h2&gt;&lt;p&gt;Right now I don’t need to make money with indie businesses. It’s totally viable for me to just enjoy the ride working full time (or maybe later part time) to achieve financial independence. I have saved enough already for a secure retirement at a typical retirement age and if I was willing to downscale my lifestyle, I would probably never have to work again. But I&amp;rsquo;m in no rush.&lt;/p&gt;&lt;p&gt;I am excited about my full time work, I get paid very well and the environment is very healthy. I feel like in the next year (or more) I have lots of opportunities for personal and professional growth without a need to grind.&lt;/p&gt;&lt;p&gt;And if anything changes, e.g. I get a awful boss - guess what, I can change teams or jobs and be alright.&lt;/p&gt;&lt;p&gt;I know that “would people pay money for this?” is a great test for a business, but it’s not the game I want to be playing now. I might later.&lt;/p&gt;&lt;h2 id=&#34;wanting-it-all&#34;&gt;Wanting it all&lt;/h2&gt;&lt;p&gt;I like to do a lot of things and “I want it all”.&lt;/p&gt;&lt;p&gt;I want to have an amazing career, be super skilled in my main job, develop my hobbies, know a lot about unrelated but interesting disciplines (e.g. biology, math, physics, psychology), learn foreign languages, and be very fit and healthy (running, climbing, gym, yoga, meditation), creative and have a thriving social life (amazing friendship, marriage and great family bonds). Also I have a thirst for adventure and traveling.&lt;/p&gt;&lt;p&gt;You see where the problem is.&lt;/p&gt;&lt;p&gt;Just having a demanding full time job and a part time side hustle could take most of your physical and mental energy, but I want so much more in life and don’t want to make sacrifices in other areas.&lt;/p&gt;&lt;p&gt;I am expecting my job to become more demanding in the next couple of months and I want to explore more of my hobbies and focus on relationships. So something has to give.&lt;/p&gt;&lt;h2 id=&#34;letting-go-vs-limping-along&#34;&gt;Letting go vs limping along&lt;/h2&gt;&lt;p&gt;Leaving things behind can be very freeing. I could maintain things with some effort, but I think that I would always feel behind and guilty of not doing more.&lt;/p&gt;&lt;p&gt;So I am going to cease all marketing efforts that I wouldn’t find enjoyable just on their own and get rid of expectations. I will let watchlimits grow on autopilot and not invest in any new premium features, unless I personally want them :) (I&amp;rsquo;m a user after all!).&lt;/p&gt;&lt;p&gt;I will also not start working on another indie product straight away.&lt;/p&gt;&lt;h2 id=&#34;did-i-waste-my-time&#34;&gt;Did I waste my time?&lt;/h2&gt;&lt;p&gt;I put a lot of time and energy into building my side projects like &lt;a href=&#34;https://readeal.app&#34;&gt;redeal.app&lt;/a&gt; (live but unmaintained), &lt;a href=&#34;https://invertimo.com&#34;&gt;invertimo.com&lt;/a&gt; (live but unmaintained), &lt;a href=&#34;https://watchlimits.com&#34;&gt;watchlimits.com&lt;/a&gt; (live and growing slowly, but not profitable).&lt;/p&gt;&lt;p&gt;With each of this projects I learned a lot and the last one &lt;a href=&#34;https://watchlimits.com&#34;&gt;watchlimits&lt;/a&gt; even earned me some subscription revenue!&lt;/p&gt;&lt;h3 id=&#34;technical-learning&#34;&gt;Technical learning&lt;/h3&gt;&lt;p&gt;I refreshed my grasp on some tech I already knew and learned new one that I didn&amp;rsquo;t know before. It was very useful as I am working in big tech that generally doesn&amp;rsquo;t use much open source, but mostly in house stuff. It also enabled me to keep my Dev skills sharp when I was working as a manager or when I just didn&amp;rsquo;t have many coding projects at my main job.&lt;/p&gt;&lt;p&gt;It generally helped with my future job prospects and overall technical competence and confidence.&lt;/p&gt;&lt;p&gt;Before I started on this journey, &amp;lsquo;can I build a complete product from scratch e2e&amp;rsquo; was an open question. There is a lot that goes into it, there are different platforms, UI, backend, billing, etc. I can confidently say now that this is an easy part. I can build everything needed, it won&amp;rsquo;t be the best ever quality, but it will be complete, solid and quick.&lt;/p&gt;&lt;h3 id=&#34;business-learning&#34;&gt;Business learning&lt;/h3&gt;&lt;p&gt;Indie hacking taught me a lot about product and business.&lt;/p&gt;&lt;p&gt;All the products that I built, even though not very successful were completely functional and were used by real people and they were rather complex.&lt;/p&gt;&lt;p&gt;It gave me a lot of food for thought in terms of &amp;rsquo;time to market&amp;rsquo;, prioritizing features, target audience and personas, user journeys, etc.&lt;/p&gt;&lt;p&gt;Do I know everything now? Obviously not, but it gave me some reps and lots of practical experience that is complementary to my professional experience.&lt;/p&gt;&lt;p&gt;I think that safely failing at some businesses taught me a lot on what to avoid in the future. So I will be in much better position later on.&lt;/p&gt;&lt;h3 id=&#34;relationship-and-audience&#34;&gt;Relationship and audience&lt;/h3&gt;&lt;p&gt;This journey for sure wasn&amp;rsquo;t lonely. Okay, at first it kinda was. But then I discovered an amazing community of indie hackers and #buildinpublic.&lt;/p&gt;&lt;p&gt;I also need to give a honorable mention to the &lt;a href=&#34;https://wannabe-entrepreneur.com/&#34;&gt;Wannabe Entrepreneur&lt;/a&gt; community! Tiago is a great community organizer and an indie hacker and made something very special.&lt;/p&gt;&lt;p&gt;Meeting fellow builders made me very happy and excited. So many talented, hard working and kind people out there! Thank you for making my journey much more enjoyable!&lt;/p&gt;&lt;h2 id=&#34;whats-next&#34;&gt;What’s next&lt;/h2&gt;&lt;p&gt;I will be prioritizing health, relationships, adventures, learning and creative endeavors. I will follow what excites me and energizes me at a given moment.&lt;/p&gt;&lt;p&gt;You might hear less from me on twitter or maybe the topics will just shift. Time will tell, but I’m excited about the new chapter!&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>My micro-side project trended on HN and got thousands of visits within the first day</title>
       <link>https://tinystruggles.com/posts/netflix_calculator_on_hn/</link>
       <pubDate>Sun, 24 Jul 2022 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/netflix_calculator_on_hn/</guid>
       <description>&lt;p&gt;My micro-side project &lt;a href=&#34;https://www.netflixcalculator.com/&#34;&gt;Netflix Calculator&lt;/a&gt; trended on Hacker News for a couple of hours and:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;received 8K+ page visits&lt;/li&gt;&lt;li&gt;brought hundreds of visitors to &lt;a href=&#34;https://watchlimits.com/&#34;&gt;watchlimits&lt;/a&gt; (my main project)&lt;/li&gt;&lt;li&gt;caused many new downloads of &lt;a href=&#34;https://watchlimits.com/&#34;&gt;watchlimits&lt;/a&gt;&lt;/li&gt;&lt;li&gt;got me the first paid customer for &lt;a href=&#34;https://watchlimits.com/&#34;&gt;watchlimits&lt;/a&gt; with a yearly plan&lt;/li&gt;&lt;li&gt;got multiple backlinks and started ranking top 5 in Google within a week - bringing SEO traffic already&lt;/li&gt;&lt;/ul&gt;&lt;blockquote class=&#34;twitter-tweet&#34;&gt;&lt;p lang=&#34;en&#34; dir=&#34;ltr&#34;&gt;OMG! My latest side project - Netflix Calculator - has been on the front page of hacker news for the last 4 hours!&lt;br&gt;&lt;br&gt;Yes, it&amp;#39;s pretty niche! But I am getting some real traffic from this! I honestly expected crickets... and it totally exceeded my expectations!&lt;a href=&#34;https://twitter.com/hashtag/buildinpublic?src=hash&amp;amp;ref_src=twsrc%5Etfw&#34;&gt;#buildinpublic&lt;/a&gt; &lt;a href=&#34;https://t.co/YNi7ckMxlJ&#34;&gt;pic.twitter.com/YNi7ckMxlJ&lt;/a&gt;&lt;/p&gt;&amp;mdash; Justyna Ilczuk ✨💪 (@attilczuk) &lt;a href=&#34;https://twitter.com/attilczuk/status/1548767187455836160?ref_src=twsrc%5Etfw&#34;&gt;July 17, 2022&lt;/a&gt;&lt;/blockquote&gt;&lt;script async src=&#34;https://platform.twitter.com/widgets.js&#34; charset=&#34;utf-8&#34;&gt;&lt;/script&gt;&lt;p&gt;Here is the traffic for the date (I put it on HN on the 17th July).&lt;img src=&#34;https://tinystruggles.com/NC_traffic.png&#34; alt=&#34;Traffic&#34;&gt;&lt;/p&gt;&lt;p&gt;Also see a big spike of active users around 17-18th of July on the active users graph:&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/watchlimits_growth.png&#34; alt=&#34;user growth&#34;&gt;&lt;/p&gt;&lt;h2 id=&#34;inspiration&#34;&gt;Inspiration&lt;/h2&gt;&lt;p&gt;Getting eyeballs on your project is hard, right? Well, it depends on the project.&lt;/p&gt;&lt;p&gt;Getting traffic to your serious landing page will probably be really hard as it&amp;rsquo;s not something that people would be excited about and would like to share. But there are some types of projects that can become viral and get a lot of attention.&lt;/p&gt;&lt;p&gt;Those sites are usually simple, fun and produce immediate value for people making them cool and easily shareable. And if executed well, can be a great way to bring qualified traffic to your main project.&lt;/p&gt;&lt;p&gt;If you are an engineer you should consider &lt;a href=&#34;https://www.failory.com/blog/side-project-marketing&#34;&gt;side-project marketing&lt;/a&gt;.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Side-project marketing consists of building a stand-alone product or service to attract potential customers to your business.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;One recent example of side project marketing I&amp;rsquo;ve seen was &lt;a href=&#34;https://bookscalculator.com/&#34;&gt;Books Calculator&lt;/a&gt; by &lt;a href=&#34;https://twitter.com/marc_louvion&#34;&gt;Marc Lou&lt;/a&gt;. The project was pretty simple and Marc has been getting crazy traffic.&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://bookscalculator.com/&#34;&gt;Books Calculator&lt;/a&gt; targets similar audience as Marc&amp;rsquo;s main product - &lt;a href=&#34;https://habitsgarden.com/&#34;&gt;Habits Garden&lt;/a&gt;. The side project was fun to build for Marc, got popular easily and was a great source of traffic to &lt;a href=&#34;https://habitsgarden.com/&#34;&gt;Habits Garden&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Was that Marc&amp;rsquo;s magic or would that be repeatable? I wanted to try it out.&lt;/p&gt;&lt;h2 id=&#34;where-did-the-idea-come-from&#34;&gt;Where did the idea come from?&lt;/h2&gt;&lt;p&gt;So why &lt;a href=&#34;https://www.netflixcalculator.com/&#34;&gt;Netflix Calculator&lt;/a&gt; in particular?&lt;/p&gt;&lt;p&gt;The idea came from working on the content marketing. I found a post on Quora asking how to check how much time one has spent on Netflix.&lt;/p&gt;&lt;p&gt;I thought that I could describe a couple of different ways, including the Netflix history file and also how to do this with &lt;a href=&#34;https://watchlimits.com/&#34;&gt;watchlimits&lt;/a&gt;, my product. And through the process of thatI found out that all the &amp;ldquo;calculators&amp;rdquo; mentioned in other articles were actually broken. Or at least broken for me.&lt;/p&gt;&lt;p&gt;That meant opportunity!&lt;/p&gt;&lt;p&gt;I wrote a &lt;a href=&#34;https://watchlimits.com/blog/posts/time_spent_on_netflix/&#34;&gt;blog post&lt;/a&gt; and I built a simple calculator online. It wasn&amp;rsquo;t very pretty, but it worked with my input and I shared the article on reddit. It had some traction.&lt;/p&gt;&lt;p&gt;I decided to double down on the calculator idea and build it similarly to the cute &lt;a href=&#34;https://bookscalculator.com/&#34;&gt;Books Calculator&lt;/a&gt; by Marc.&lt;/p&gt;&lt;p&gt;The reasons why I thought it was a good candidate for a side project marketing was that:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;it would be quick to build (I had a prototype already)&lt;/li&gt;&lt;li&gt;it refers to the name brand&lt;/li&gt;&lt;li&gt;people are curious and the product delivers value quickly (just input the stats)&lt;/li&gt;&lt;li&gt;I could use Marc&amp;rsquo;s project as a reference&lt;/li&gt;&lt;li&gt;it would be a nice way to grow my design and frontend skills (I used it to play it with some new technologies as well)&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;what-next&#34;&gt;What next?&lt;/h2&gt;&lt;p&gt;Even though this launch greatly exceeded my expectations, I&amp;rsquo;m far from done.&lt;/p&gt;&lt;p&gt;I will continue marketing &lt;a href=&#34;https://www.netflixcalculator.com/&#34;&gt;Netflix Calculator&lt;/a&gt; and learning from the experience.&lt;/p&gt;&lt;p&gt;For now I&amp;rsquo;m not planning to put a lot of development time in &lt;a href=&#34;https://www.netflixcalculator.com/&#34;&gt;Netflix Calculator&lt;/a&gt;, but get the basics right - for example making sure it works for all sort of countries. But I don&amp;rsquo;t dismiss the opportunity of making it much more advanced in the future as I am just scratching the surface with the analytics.&lt;/p&gt;&lt;p&gt;One thing that prevents me from going full out is that I still want to make various improvements to &lt;a href=&#34;https://watchlimits.com/&#34;&gt;watchlimits&lt;/a&gt; not to waste the opportunity all this high traffic is bringing.I timed the first launch of the &lt;a href=&#34;https://www.netflixcalculator.com/&#34;&gt;Netflix Calculator&lt;/a&gt; with releasing the first premium feature of &lt;a href=&#34;https://watchlimits.com/&#34;&gt;watchlimits&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Insights from this first launch helped me to get significant improvements already and there are many more to be done.&lt;/p&gt;&lt;p&gt;Next launches for the calculator:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&#34;https://www.reddit.com/r/InternetIsBeautiful/&#34;&gt;https://www.reddit.com/r/InternetIsBeautiful/&lt;/a&gt; (next Tuesday)&lt;/li&gt;&lt;li&gt;Productivity related subreddits like r/Productivity or r/GetDisciplined or r/NoSurf (~next Thursday or weekend?)&lt;/li&gt;&lt;li&gt;Product Hunt (early August)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It&amp;rsquo;s pretty scary, so wish me luck!&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Technical learnings from the first quarter of work on watchlimits</title>
       <link>https://tinystruggles.com/posts/technical_learnings_watchlimits/</link>
       <pubDate>Fri, 22 Jul 2022 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/technical_learnings_watchlimits/</guid>
       <description>&lt;p&gt;You can still use side projects to grow as an engineer without sacrificing the business success of your product.&lt;/p&gt;&lt;p&gt;There are no guarantees that the business will succeed, but I believe that regardless of the business outcome you should try to end up smarter than when you started.&lt;/p&gt;&lt;p&gt;This post is a retrospective of my technical learnings from my first 3 months of working on &lt;a href=&#34;https://watchlimits.com&#34;&gt;watchlimits.com&lt;/a&gt; (chrome extension to limit excessive video watching). I will separately release a post on my business/marketing learnings as well.&lt;/p&gt;&lt;p&gt;Majority of my professional experience (last 6+ years) is with high performance, distributed storage systems written in C++, reliability and devops. I have some full-stack experience from before then, so I&amp;rsquo;m not completely new to it, but working on side projects for sure helps me to maintain and develop an engineering skillset relevant to building a wider array of customer facing products.&lt;/p&gt;&lt;p&gt;So here are some things that &lt;a href=&#34;https://watchlimits.com&#34;&gt;watchlimits&lt;/a&gt; helped me learn/improve at during the first 3 months:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Chrome extension development&lt;/li&gt;&lt;li&gt;Frontend tooling and practices&lt;/li&gt;&lt;li&gt;Design and UX&lt;/li&gt;&lt;li&gt;Mastering ReactJS&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;chrome-extension-development&#34;&gt;Chrome extension development&lt;/h3&gt;&lt;p&gt;Before working on &lt;a href=&#34;https://watchlimits.com&#34;&gt;watchlimits&lt;/a&gt; I had very little knowledge about developing chrome extensions.&lt;/p&gt;&lt;p&gt;And instead of working on something simple I went for a full blown app. I had to learn fast! &lt;a href=&#34;https://developer.chrome.com/docs/extensions/reference/&#34;&gt;Chrome documentation&lt;/a&gt; was my main source of info, but the documentation is a bit scarce.&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://watchlimits.com&#34;&gt;watchlimits&lt;/a&gt; consists of 4 basic parts:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;popup (React App)&lt;/li&gt;&lt;li&gt;content script (Injected React App)&lt;/li&gt;&lt;li&gt;settings page (React App)&lt;/li&gt;&lt;li&gt;background script (just JS app)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I set it up building with webpack with the output files being configure in the manifest file. I set up everything from scratch, before I bought a &lt;a href=&#34;https://chromeextensionkit.com/&#34;&gt;Chrome Extension Kit&lt;/a&gt;. The kit contains code examples of multiple simple apps and an ebook with tips.&lt;/p&gt;&lt;p&gt;Overall the &lt;a href=&#34;https://chromeextensionkit.com/&#34;&gt;Chrome Extension Kit&lt;/a&gt; was good but I bought it too late in the process. I have already figured out most of the things beforehand. I plan to use boilerplate samples for my new projects.&lt;/p&gt;&lt;p&gt;Even though the kit provided a bunch of technical solutions, I still had some important problems left to solve by myself.&lt;/p&gt;&lt;p&gt;The most complex parts I had to develop were around:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;storing data &amp;amp; communication between different parts of the extension&lt;/li&gt;&lt;li&gt;updating the extension that runs live scripts (if the background script gets reloaded the old injected content scripts lose connectivity to the background script which was causing issues)&lt;/li&gt;&lt;li&gt;keeping the background script alive with manifest v3 (much harder than it should be)&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;frontend-tooling---storybook&#34;&gt;Frontend tooling - Storybook&lt;/h3&gt;&lt;p&gt;My biggest discovery in the frontend tooling was storybook. I heard about it before, but I wasn&amp;rsquo;t sure how much work would it be to set it up. It turned ou to be very simple and the payoff was amazing.&lt;/p&gt;&lt;p&gt;Storybook stories provide a great way to develop complex presentation components in isolation.&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/storybook.png&#34; alt=&#34;storybook&#34;&gt;&lt;/p&gt;&lt;p&gt;This makes UI development much easier and it can also provide a great alternative to headless integration testing.&lt;/p&gt;&lt;h3 id=&#34;design--ux&#34;&gt;Design / UX&lt;/h3&gt;&lt;p&gt;I continue to find design to be quite fascinating. I think I am good with basics at this point (contrast, color, spacing, visual hierarchy, etc). But I still have a lot too learn!&lt;/p&gt;&lt;p&gt;Some highlights from this period were:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;design systems and &lt;a href=&#34;https://medium.com/eightshapes-llc/tokens-in-design-systems-25dd82d58421&#34;&gt;design tokens&lt;/a&gt;&lt;/li&gt;&lt;li&gt;learning more about common UX patterns like using a modal vs a sub page, where to put buttons on forms or whether or not to use tooltips&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I have also found this amazing &lt;a href=&#34;https://adamsilver.io&#34;&gt;blog&lt;/a&gt; by Adam Silver, full of incredible value articles on UI best practices.&lt;/p&gt;&lt;p&gt;Some of my favorite articles:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&#34;https://adamsilver.io/blog/the-problem-with-tooltips-and-what-to-do-instead/&#34;&gt;https://adamsilver.io/blog/the-problem-with-tooltips-and-what-to-do-instead/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://adamsilver.io/blog/where-to-put-buttons-on-forms/&#34;&gt;https://adamsilver.io/blog/where-to-put-buttons-on-forms/&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;more-reactjs&#34;&gt;More ReactJS&lt;/h3&gt;&lt;p&gt;Watchlimits is composed of 3 React Apps!&lt;/p&gt;&lt;p&gt;One of the interesting challenges that I have encountered was mixing &lt;code&gt;useState&lt;/code&gt; and periodic closures.&lt;/p&gt;&lt;p&gt;I also made my first NPM package for MUI components integrated with formik, so I can share it between my projects.&lt;/p&gt;&lt;p&gt;The main external libraries I&amp;rsquo;ve been using were:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;MUI (UI components)&lt;/li&gt;&lt;li&gt;nivo (graphs and charts)&lt;/li&gt;&lt;li&gt;formik (easier form handling)&lt;/li&gt;&lt;li&gt;react router v6&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;And I had to learn the newer version of the react-router as it was quite different from the previous one.&lt;/p&gt;&lt;p&gt;Overall, I have been keeping things simple and almost all of my components have been using functional components and react hooks.&lt;/p&gt;&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;&lt;p&gt;Every project gives us opportunities to learn. Working on indie products we need to balance learning engineering skills with creating value for the customers, but I believe we can do both without too much sacrifice.&lt;/p&gt;&lt;p&gt;What have you been learning with your projects? If you liked this article you can follow me on &lt;a href=&#34;https://twitter.com/attilczuk&#34;&gt;twitter&lt;/a&gt;!&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>My release checklist for chrome extensions</title>
       <link>https://tinystruggles.com/posts/extension_release_checklist/</link>
       <pubDate>Sat, 09 Jul 2022 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/extension_release_checklist/</guid>
       <description>&lt;p&gt;Many products can be updated very quickly. You spot a bug, you push a fix in next 5 minutes, sadly for chrome extensions it&amp;rsquo;s not like that.&lt;/p&gt;&lt;p&gt;When you update a chrome extension in the chrome web store you submit a package that then goes through a lengthy review. It can take multiple days and during that time you can&amp;rsquo;t update a newer version, for example if you find some bugs.&lt;/p&gt;&lt;p&gt;That means that you better deploy solid code otherwise your extension might be broken for multiple days (if the new review takes similar amount of time).&lt;/p&gt;&lt;p&gt;Maybe in some cases of a simpler extensions the review can be simpler, but in the case of my extension &lt;a href=&#34;https://watchlimits.com&#34;&gt;watchlimits&lt;/a&gt; it&amp;rsquo;s always been painfully slow!&lt;/p&gt;&lt;p&gt;So how do I make sure that what I release is solid?&lt;/p&gt;&lt;p&gt;I go through a checklist.&lt;/p&gt;&lt;p&gt;Here it is:&lt;/p&gt;&lt;p&gt;Checklist what to do before releasing:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;clean up debugging statements&lt;/li&gt;&lt;li&gt;think about main happy and unhappy paths and see if they are covered&lt;/li&gt;&lt;li&gt;update the manifest&lt;/li&gt;&lt;li&gt;build prod version of the extension (I develop in dev mode)&lt;/li&gt;&lt;li&gt;test manually&lt;/li&gt;&lt;li&gt;run tests&lt;/li&gt;&lt;li&gt;run storybook tests&lt;/li&gt;&lt;li&gt;click through storybook stories - all should work and look fine&lt;/li&gt;&lt;li&gt;install extension from unpacked version in chromium (new user experience)&lt;/li&gt;&lt;li&gt;test manually&lt;/li&gt;&lt;li&gt;commit any code changes&lt;/li&gt;&lt;li&gt;upload!&lt;/li&gt;&lt;li&gt;write a changelog&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;hard-lessons&#34;&gt;Hard lessons&lt;/h3&gt;&lt;h3 id=&#34;cleanups-and-unhappy-paths&#34;&gt;Cleanups and unhappy paths&lt;/h3&gt;&lt;p&gt;The cleanups are pretty self explanatory, no one like chrome extensions to spam their console. But what about thinking about the unhappy paths?&lt;/p&gt;&lt;p&gt;I have the &amp;ldquo;unhappy paths&amp;rdquo; on my checklist, because building new features is so exciting it&amp;rsquo;s really easy to forget about major &amp;ldquo;unhappy&amp;rdquo; aspects of the feature!But those matter too! Examples would be - extension is offline or there is no data, etc&amp;hellip;&lt;/p&gt;&lt;p&gt;And that can add a significant amount of coding so I do this before any serious manual testing.&lt;/p&gt;&lt;h3 id=&#34;testing--storybook&#34;&gt;Testing &amp;amp; storybook&lt;/h3&gt;&lt;p&gt;As your software grows in complexity, it&amp;rsquo;s easy to miss that something is broken, so testing is key.&lt;/p&gt;&lt;p&gt;It could be manual testing or automated testing. Great automated testing of mostly frontend products is pretty hard. I try to make sure to have good unit tests, but I also rely on&lt;a href=&#34;https://storybook.js.org/&#34;&gt;storybook&lt;/a&gt; which is incredible for making sure that main subsets of my app are working fine. I also love it for isolated development. I highly recommend trying it out!&lt;/p&gt;&lt;p&gt;Before I release a new version I would look through storybook stories to make sure that everything looks good.&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/storybook.png&#34; alt=&#34;storybook&#34;&gt;&lt;/p&gt;&lt;p&gt;For manual testing there is nothing better than recording a video demo. Because it will force you to go through the whole functionality end to end.&lt;/p&gt;&lt;p&gt;Not only you will make sure that the UX is good, you will easily spot all the bugs that you wouldn&amp;rsquo;t spot otherwise.&lt;/p&gt;&lt;h3 id=&#34;fresh-user-installation&#34;&gt;Fresh user installation&lt;/h3&gt;&lt;p&gt;If your extension relies on some persistent data, it&amp;rsquo;s really important to check if the extension will work for new users as well as existing users.&lt;/p&gt;&lt;p&gt;I have made this mistake before! I was developing the feature iteratively and didn&amp;rsquo;t realize that some functionality required some data to be already persisted and that resulted in a broken experience for new users. Not the best first impression!&lt;/p&gt;&lt;p&gt;Because of that I pack the extension, unzip it in a different folder and run it a separate browser as a fresh installation.&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/fresh_installation.png&#34; alt=&#34;fresh&#34;&gt;&lt;/p&gt;&lt;p&gt;And then I manually test it again&amp;hellip;&lt;/p&gt;&lt;h3 id=&#34;changelog&#34;&gt;Changelog?&lt;/h3&gt;&lt;p&gt;My app doesn&amp;rsquo;t have a built in changelog in it, but writing about any big feature release helps me better articulate the value of the feature and also explain how to use it.I can use it for the promotion or as a knowledge base for the users.&lt;/p&gt;&lt;p&gt;Time will tell if it&amp;rsquo;s a worthwhile effort!&lt;/p&gt;&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;&lt;p&gt;I&amp;rsquo;m an indie maker, so I don&amp;rsquo;t have an infrastructure team or time for building a complex CI pipeline or automating all the testing.&lt;/p&gt;&lt;p&gt;What about you? Are you careful with your releases? Do you have any recommendations to improve my checklist?&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>How I find time for indie hacking while having a full time job</title>
       <link>https://tinystruggles.com/posts/time_for_side_projects/</link>
       <pubDate>Fri, 03 Jun 2022 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/time_for_side_projects/</guid>
       <description>&lt;p&gt;A friend recently asked me:&lt;/p&gt;&lt;p&gt;“How do you find time for all your projects while working full time?”&lt;/p&gt;&lt;p&gt;“I don’t, I just complain about not having enough time instead.”&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/rushed_hu_d129ba009a121cd6.jpg&#34;&gt;&lt;p&gt;But more seriously, I am working on two products, while having a demanding full time job and doing a lot of sports (7+ training sessions per week).&lt;/p&gt;&lt;p&gt;Ok, I’m in my late twenties and I don’t have kids, but I do have a social life and a spouse that wants to do stuff together.I still don’t have as much time to work on my projects as I would like to, but I think I make the best out of it considering my priorities.&lt;/p&gt;&lt;p&gt;So, what are my tricks?&lt;/p&gt;&lt;h2 id=&#34;setting-clear-priorities&#34;&gt;Setting clear priorities&lt;/h2&gt;&lt;blockquote&gt;&lt;p&gt;“Focus is about saying no” - Steve Jobs&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;More often than not, you can’t find the time to do the things you want to do, because you are doing too many things already.&lt;/p&gt;&lt;p&gt;I could be working on so many projects, doing so many hobbies and learning so many things!&lt;/p&gt;&lt;p&gt;Yes, I definitely suffer from the shiny object syndrome.&lt;/p&gt;&lt;p&gt;I could be making prioritization decisions every day, but then I would probably end up switching my focus a lot and not being focused in the end. Because of that I like to pick the big “rocks” for the quarter: milestones, commitments, themes.&lt;/p&gt;&lt;p&gt;Why quarterly? For some people months, years or decades might be better, but I really like the quarters as they provide a nice balance between short and long term.&lt;/p&gt;&lt;p&gt;Once I set my priorities, I also set “not a priority” for other stuff. That really helps me with finding time as this cuts out a lot of potential ways of spending my time.&lt;/p&gt;&lt;p&gt;For example one of my priorities for this quarter is growing and getting feedback for &lt;a href=&#34;https://watchlimits.com&#34;&gt;watchlimits&lt;/a&gt;, not launching some new cool project or e.g. exploring a new tech framework.&lt;/p&gt;&lt;h2 id=&#34;keeping-work-hours-within-limits&#34;&gt;Keeping work hours within limits&lt;/h2&gt;&lt;p&gt;My job could consume all my available time. I have a great job, but I have a vast scope of responsibility and a never ending todo list. If the more urgent stuff is dealt with, there is always lots of important long term stuff and every week brings some new priorities and interruptions. It would never end and to make it even worse there is stuff happening 24/7. When I finish work, people in USA often start their work days, then people in India. It’s always on.&lt;/p&gt;&lt;p&gt;Burnout is a real thing and it often starts with working too much. I get paid for 8 hours so I will be working 8 hours per day and if I feel too tired before 8 hours are up, I will cut the day short.&lt;/p&gt;&lt;p&gt;A person can rationalize staying up late, working over weekends, etc. “No one else can do it”, “I have this important deadline”, but really what’s the worst thing that is going to happen if you don’t finish this thing?&lt;/p&gt;&lt;p&gt;Maybe you will disappoint someone. They will likely understand. Also, you will have to learn how to better plan to be in a situation when you are late less often.&lt;/p&gt;&lt;p&gt;Anticipation &amp;amp; planning help with avoiding all sorts of &amp;ldquo;OMG, we have to stay late for X&amp;rdquo; problems.&lt;/p&gt;&lt;p&gt;But a different likely outcome is that people will respect you for having boundaries, and you will be a healthy example for other team mates.&lt;/p&gt;&lt;h2 id=&#34;cutting-out-the-time-sinks&#34;&gt;Cutting out the time sinks&lt;/h2&gt;&lt;p&gt;Everyone has their own time sinks. For me it used to be video games and watching videos like netflix or youtube.&lt;/p&gt;&lt;p&gt;It’s engaging, it’s enjoyable, but then most of your free time is gone!&lt;/p&gt;&lt;p&gt;Obviously, it’s not always bad, you might decide that those things are really important to you and keep them as your priority. But you might also realize that they prevent you from doing the things you really want to do. Like working on the product on the side or learning skills to change jobs.&lt;/p&gt;&lt;p&gt;These days there are so many great shows to watch on netflix and it’s so easy to binge content. Similarly for youtube, the interesting content is never ending. The algorithm will try to keep you glued to the screen.&lt;/p&gt;&lt;p&gt;What I use day to day is:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Disabling apps on android (gmail on corporate phone, sometimes chrome on my personal phone)&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://blocksite.co/&#34;&gt;Blocksite&lt;/a&gt; - I use the free version&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://watchlimits.com&#34;&gt;watchlimits&lt;/a&gt; extension (that I built myself) - that one specifically help me not to fall into “one more netflix episode trap”.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I also wrote an &lt;a href=&#34;https://watchlimits.com/blog/posts/more_hours_per_week/#step-3-use-tools-to-limit-the-suboptimal-habits&#34;&gt;article&lt;/a&gt; with a 3 step framework to find additional time by reexamining your digital habits.&lt;/p&gt;&lt;p&gt;If you cut out your major time sinks out of your life you will find yourself with lots of unoccupied time. For some it might be super boring, for me it feels luxurious.&lt;/p&gt;&lt;h2 id=&#34;summary&#34;&gt;Summary&lt;/h2&gt;&lt;p&gt;There is no magic here, but those 3 simple tricks work well for me:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Setting my priorities clear&lt;/li&gt;&lt;li&gt;Keeping my work hours capped&lt;/li&gt;&lt;li&gt;Avoiding excessive time sinks&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It&amp;rsquo;s still hard to work on the products part time and I have more or less productive weeks, but I try to make regular progress.&lt;/p&gt;&lt;p&gt;Feel free to share what works for you! You can follow me on &lt;a href=&#34;https://twitter.com/attilczuk&#34;&gt;twitter&lt;/a&gt;!&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>How to make a product on gumroad and earn some money (#secretunasaladrecipe)</title>
       <link>https://tinystruggles.com/posts/building_my_first_gumroad_product/</link>
       <pubDate>Sat, 09 Apr 2022 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/building_my_first_gumroad_product/</guid>
       <description>&lt;p&gt;If you&amp;rsquo;ve been in the Indie Hackers circles for a while you probably heard about people like dvassallo@ who &lt;a href=&#34;https://www.indiehackers.com/post/how-i-made-210-822-selling-a-pdf-and-a-video-on-the-internet-028d5b0a2c&#34;&gt;made hundreds of thousands of dollars&lt;/a&gt; selling simple digital products on gumroad.&lt;/p&gt;&lt;p&gt;Make a thing, put it on gumroad, earn money, could it all be that simple?&lt;/p&gt;&lt;p&gt;I have heard many good things about the platform and I wanted to try it out with something straighforward. I didn’t feel like I had some super valuable secret I could share that I could make into a killer course, but&amp;hellip;&lt;/p&gt;&lt;p&gt;I had the incredible secret tuna salad recipe (&lt;a href=&#34;https://tinystruggles.com/posts/secret_tuna_salad_recipe_motivations/&#34;&gt;motivations&lt;/a&gt;) though and I decided tomake it into a gumroad product and I even made some money with it.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/tuna/sadly_not_hundreds_of_thousands_hu_f6c5a2303517100b.png&#34;&gt;&lt;p&gt;If you think I should be earning more money from this, &lt;a href=&#34;https://attilczuk.gumroad.com/l/ykpqau&#34;&gt;here&lt;/a&gt; is your link to buy it :D.&lt;/p&gt;&lt;p&gt;In this article I will explain what went into making &amp;ldquo;the secret tuna salad recipe&amp;rdquo;, the digital product.&lt;/p&gt;&lt;h2 id=&#34;ideation-and-planning&#34;&gt;Ideation and planning&lt;/h2&gt;&lt;p&gt;What should be included in the product? I had a couple of ideas.&lt;/p&gt;&lt;p&gt;The core of the recipe should be in a PDF as it can be good to present lists, steps and explain the details&lt;/p&gt;&lt;p&gt;I was thinking about making a video but video production of recipes is a pretty big undertaking and since I’m not a pro youtuber the video production quality would likely be poor (my husband also started laundry so that eliminated the possibility of shooting the video that day).&lt;/p&gt;&lt;p&gt;I was also thinking about including some bonus content with tuna or fish jokes, but I decided that I’m not that funny and I couldn’t find enough funny jokes&amp;hellip;&lt;/p&gt;&lt;p&gt;At the end you have to play to your strengths. Including something of poorer quality might dilute the value of the product and leave the customer disappointed.&lt;/p&gt;&lt;p&gt;I used notion for planning and brainstorming ideas. I also wrote there my shopping list, ingredient lists and all the steps of the recipe.&lt;/p&gt;&lt;p&gt;Once I had all the ingredients ready, I decided to take detailed photos of the required equipment and ingredients and later document each step of making the recipe.&lt;/p&gt;&lt;h2 id=&#34;gear-and-tools&#34;&gt;Gear and tools&lt;/h2&gt;&lt;p&gt;I have a tripod I could use, but it wasn’t necessary as I wasn’t really taking pictures of myself, but of things.&lt;/p&gt;&lt;p&gt;I took handheld pictures with the Pixel 6 (maybe one or two were taken by my husband when I needed an extra hand).&lt;/p&gt;&lt;p&gt;I am a Google Photos user and my photos were quickly available on my computer.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/tuna/ingredients_photo_hu_c3d8ef96867b940c.jpg&#34;&gt;&lt;h2 id=&#34;how-to-make-a-nice-pdf-with-google-slides&#34;&gt;How to make a nice PDF with Google slides&lt;/h2&gt;&lt;p&gt;I use linux and I don’t have Microsoft Office installed, but I know that it’s pretty easy to make a PDF out of Google Slides and it was my tool of choice.&lt;/p&gt;&lt;p&gt;For the PDF to look good, I wanted to use a consistent visual theme. I decided to make my own visual theme out of blue with a small touch of yellow and Nunito font.&lt;/p&gt;&lt;p&gt;The screenshot below shows how to make your custom theme in Google Slides (&lt;code&gt;Slide &amp;gt; Edit theme&lt;/code&gt;):&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/tuna/edit_theme_hu_17f1a4c5fb6fe04.png&#34;&gt;&lt;p&gt;Once you have your slides ready, you can easily convert them to PDF with &lt;code&gt;File &amp;gt; Download &amp;gt; PDF Document&lt;/code&gt;.&lt;/p&gt;&lt;h2 id=&#34;visuals&#34;&gt;Visuals&lt;/h2&gt;&lt;p&gt;I wanted to add some nice visuals to make the recipe more interesting and I searched around for some good visuals. One obvious source of expressive images is of course emojis, but I wanted something more distinct.&lt;/p&gt;&lt;p&gt;Also, I wanted it quickly and ideally for free. I could draw something myself, but that wouldn&amp;rsquo;t be quick and I&amp;rsquo;m pretty bad at digital drawings as well, so that option was out of the question.&lt;/p&gt;&lt;p&gt;My usual source of visuals is a fantastic &lt;a href=&#34;https://www.pexels.com/search/tuna/&#34;&gt;pexels.com&lt;/a&gt;, but I didn&amp;rsquo;t see anything that would match my artistic vision (and the type of tuna I was using).&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/tuna/wrong_tuna_hu_2cb0282b8f7964b2.png&#34;&gt;&lt;p&gt;I had good luck as Wikipedia had lots of great tuna illustrations in the public domain! Score!This is how the mister helpful tuna was born:&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/Tuna_hu_e5c02259a44a187b.png&#34;&gt;&lt;h2 id=&#34;setting-up-the-gumroad-page&#34;&gt;Setting up the gumroad page&lt;/h2&gt;&lt;p&gt;Once I had my content, my visuals and a generated PDF it was time to put the product on &lt;a href=&#34;https://gumroad.com/&#34;&gt;Gumroad&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I expected it to be much harder, but it&amp;rsquo;s surprisingly simple.&lt;/p&gt;&lt;p&gt;You start with the name and a price:&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/tuna/gumroad_simple_hu_66614bee8fbf893e.png&#34;&gt;&lt;p&gt;And then you can customize what will be shown to the user and what files they get when they buy the product.&lt;/p&gt;&lt;p&gt;Probably the longest part of the process for me was setting up the images, especially because the thumbnail had to be of very specific size.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/tuna/gumroad_visuals_hu_77accb53eec61baf.png&#34;&gt;&lt;p&gt;For the selling copy, I decided to keep it short and simple!&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/tuna/gumroad_copy_hu_ce5b18b267516619.png&#34;&gt;&lt;p&gt;If you have some suggestions, I would be happy to hear them.&lt;/p&gt;&lt;h2 id=&#34;the-rest-is-history-marketing&#34;&gt;The rest is &lt;s&gt;history&lt;/s&gt; marketing!&lt;/h2&gt;&lt;p&gt;Once you hit publish, it&amp;rsquo;s out there and people can buy it.And some did!&lt;/p&gt;&lt;p&gt;However, as with any product you can&amp;rsquo;t just create it and wait for people to come to you, you need to market it.And that&amp;rsquo;s a completely different story :).&lt;/p&gt;&lt;p&gt;From the gumroad side, it has some useful tools for marketers such as analytics (to track where people came from and which sources convert the best) as well as it collects emails of your customers and that can be used for getting feedback on the product, sending updates or upselling.&lt;/p&gt;&lt;img src=&#34;https://tinystruggles.com/images/tuna/gumroad_stats_hu_8f7216591ce5e4cc.png&#34;&gt;&lt;p&gt;If you enjoyed this post, you can follow me on &lt;a href=&#34;https://twitter.com/attilczuk&#34;&gt;twitter&lt;/a&gt; or support me by getting &lt;a href=&#34;https://attilczuk.gumroad.com/l/ykpqau&#34;&gt;the tuna salad recipe&lt;/a&gt; or one of my other &lt;a href=&#34;https://attilczuk.gumroad.com/&#34;&gt;gumroad projects&lt;/a&gt;. Keep on hacking!&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Sometimes stupid product ideas are the best - motivation behind the secret tuna salad recipe</title>
       <link>https://tinystruggles.com/posts/secret_tuna_salad_recipe_motivations/</link>
       <pubDate>Thu, 17 Mar 2022 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/secret_tuna_salad_recipe_motivations/</guid>
       <description>&lt;p&gt;I created a &lt;a href=&#34;https://attilczuk.gumroad.com/l/ykpqau&#34;&gt;digital product&lt;/a&gt; out of my family&amp;rsquo;s favorite tuna salad recipe and in this article I will tell you why I thought it was a good idea.&lt;/p&gt;&lt;p&gt;I’m a software engineer by trade and I have spent thousands of hours building software products that don’t really earn me money (yet).&lt;/p&gt;&lt;p&gt;Entrepreneurship is risky, that’s for sure, but often indie businesses fail because founders lack marketing skills and fear to put themselves out there until it’s pretty late. And then it turns out that they have been building a product that no one wants.&lt;/p&gt;&lt;h2 id=&#34;my-inspiration&#34;&gt;My inspiration&lt;/h2&gt;&lt;p&gt;During my last vacation I got a bit fixated on a weird idea - making a digital product out of my family&amp;rsquo;s favorite salad recipe.&lt;/p&gt;&lt;p&gt;What got me excited was that:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;it&amp;rsquo;s a great tasting, healthy salad and it&amp;rsquo;s very easy to make&lt;/li&gt;&lt;li&gt;it could be fun to make a big deal out of it and sell the recipe as a digital product on gumroad&lt;/li&gt;&lt;li&gt;in a process I could learn: marketing, SEO, ads&amp;hellip; and popularize a great salad so what would be there to lose apart from my time?&lt;/li&gt;&lt;/ul&gt;&lt;blockquote class=&#34;twitter-tweet&#34;&gt;&lt;p lang=&#34;en&#34; dir=&#34;ltr&#34;&gt;The &lt;a href=&#34;https://twitter.com/hashtag/secrettunasaladrecipe?src=hash&amp;amp;ref_src=twsrc%5Etfw&#34;&gt;#secrettunasaladrecipe&lt;/a&gt; operation is on!&lt;br&gt;I will make a digital product out of it and sell it on gumroad!&lt;br&gt;&lt;br&gt;Today&amp;#39;s progress:&lt;br&gt;- bought a domain &lt;a href=&#34;https://t.co/Fs1k2KfqR2&#34;&gt;https://t.co/Fs1k2KfqR2&lt;/a&gt;&lt;br&gt;- keyword research&lt;br&gt;- failed to make the salad due to a lack of a key ingredient (not tuna)&lt;a href=&#34;https://twitter.com/hashtag/buildinpublic?src=hash&amp;amp;ref_src=twsrc%5Etfw&#34;&gt;#buildinpublic&lt;/a&gt; &lt;a href=&#34;https://t.co/15LnxFWOSF&#34;&gt;pic.twitter.com/15LnxFWOSF&lt;/a&gt;&lt;/p&gt;&amp;mdash; Justyna Ilczuk ✨💪 (@attilczuk) &lt;a href=&#34;https://twitter.com/attilczuk/status/1501281119580295168?ref_src=twsrc%5Etfw&#34;&gt;March 8, 2022&lt;/a&gt;&lt;/blockquote&gt;&lt;script async src=&#34;https://platform.twitter.com/widgets.js&#34; charset=&#34;utf-8&#34;&gt;&lt;/script&gt;&lt;p&gt;If something is good, making money from it is just a matter of solid marketing, right?&lt;/p&gt;&lt;h2 id=&#34;are-you-mad-this-cant-be-a-good-business-idea-right&#34;&gt;Are you mad, this can’t be a good business idea, right?&lt;/h2&gt;&lt;p&gt;People are used to seeing recipes online for free. It doesn’t make sense to put in a lot of effort if people can access countless good recipes without paying. The recipes are too common.&lt;/p&gt;&lt;p&gt;However, you could say the same about books, there are countless books out there, many of them accessible for free at your local library. But sometimes you want a specific book, maybe because it was marketed to you recently or because others recommended it. It’s not a huge cost and you just buy it. I think in this case it can be similar, as long as it prices accessibly, I think people would be ok to pay for it.&lt;/p&gt;&lt;p&gt;The other argument against this project, is that no one will want it. Who is interested in tuna salad anyway? Well, the basic research shows otherwise:&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/tunasaladkeywords.png&#34; alt=&#34;tuna salad keywords&#34;&gt;&lt;/p&gt;&lt;p&gt;There are between 100k to 1 Million monthly google searches for the &amp;ldquo;tuna salad&amp;rdquo; alone. And there more other keywords that look interesting too. The competition seems low. I smell opportunity!&lt;/p&gt;&lt;h2 id=&#34;what-makes-this-idea-a-good-business&#34;&gt;What makes this idea a good business?&lt;/h2&gt;&lt;p&gt;The underlying product is really good and the founder (me) believes in it. There is a good reason why this recipe is so popular among my extended family. Because of that I am happy to put great effort behind it even if it’s a bit silly.&lt;/p&gt;&lt;p&gt;Another great reason is that there are no ongoing costs and no costs of physical goods. Once the recipe is packaged into a product it can be sold at no additional cost! I plan to improve the product by giving location based tips (due to accessibility of the ingredients), but apart from that there is no need to ever change it as the recipe itself is pretty good.&lt;/p&gt;&lt;p&gt;This product is also a great way to learn marketing. There is no better way of learning than by actually doing and this product will provide countless opportunities for marketing and in many ways it’s better for learning marketing than if it was a serious SaaS startup.&lt;/p&gt;&lt;h2 id=&#34;learning-business-on-digital-products-vs-saas&#34;&gt;Learning business on digital products vs SaaS&lt;/h2&gt;&lt;p&gt;Building a successful SaaS is really hard and there is a huge start up cost of building a good product before being able to sell it and it being good enough to be really used by customers.&lt;/p&gt;&lt;p&gt;It&amp;rsquo;s easy to get bogged down with all the complexity. It’s better to start with something simpler.&lt;/p&gt;&lt;p&gt;Here&amp;rsquo;s a quote from  &lt;a href=&#34;https://robwalling.com/2015/03/26/the-stairstep-approach-to-bootstrapping/&#34;&gt;Rob Walling blog post on stairstep approach&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;In my experience, the biggest pitfall that trips up first-time product people is trying to create something too complex.The strategy that seems to give people the best chance of success is creating a simple product, with a simple marketing plan – one that only requires a single traffic channel.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;I&amp;rsquo;m not the only one thinking this way!&lt;/p&gt;&lt;h2 id=&#34;still-sounds-fishy&#34;&gt;Still sounds fishy?&lt;/h2&gt;&lt;p&gt;In the end I am doing this because it’s original and fun.&lt;/p&gt;&lt;p&gt;The idea brings a smile to my face, every time I think about it. And I hope other people will appreciate the weirdness of it too :).&lt;/p&gt;&lt;p&gt;So yeah, now I sell a secret tuna salad recipe online!Wanna &lt;a href=&#34;https://attilczuk.gumroad.com/l/ykpqau&#34;&gt;buy&lt;/a&gt;?&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Decimals in python, Django and your DB</title>
       <link>https://tinystruggles.com/posts/django_decimals/</link>
       <pubDate>Sun, 13 Feb 2022 08:55:59 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/django_decimals/</guid>
       <description>&lt;div class=&#34;toc&#34;&gt;    &lt;h3&gt;Table of contents&lt;/h3&gt;    &lt;nav id=&#34;TableOfContents&#34;&gt;  &lt;ul&gt;    &lt;li&gt;&lt;a href=&#34;#why-use-decimals&#34;&gt;Why use decimals?&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#decimals-in-action&#34;&gt;Decimals in action&lt;/a&gt;      &lt;ul&gt;        &lt;li&gt;&lt;a href=&#34;#decimals-in-python&#34;&gt;Decimals in python&lt;/a&gt;&lt;/li&gt;        &lt;li&gt;&lt;a href=&#34;#decimals-in-django&#34;&gt;Decimals in Django&lt;/a&gt;&lt;/li&gt;        &lt;li&gt;&lt;a href=&#34;#decimals-in-the-sql-database&#34;&gt;Decimals in the SQL database&lt;/a&gt;&lt;/li&gt;      &lt;/ul&gt;    &lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#downsides-and-pitfalls&#34;&gt;Downsides and pitfalls&lt;/a&gt;      &lt;ul&gt;        &lt;li&gt;&lt;a href=&#34;#mixing-floats-and-decimals&#34;&gt;Mixing floats and decimals&lt;/a&gt;&lt;/li&gt;        &lt;li&gt;&lt;a href=&#34;#if-you-create-decimals-from-floats-you-are-loosing-accuracy&#34;&gt;If you create decimals from floats you are loosing accuracy&lt;/a&gt;&lt;/li&gt;        &lt;li&gt;&lt;a href=&#34;#default-context-vs-local-context&#34;&gt;Default context vs local context&lt;/a&gt;&lt;/li&gt;        &lt;li&gt;&lt;a href=&#34;#django-pitfall-formsserializers-out-of-sync-with-model-field-definition&#34;&gt;Django pitfall: forms/serializers out of sync with model field definition&lt;/a&gt;&lt;/li&gt;        &lt;li&gt;&lt;a href=&#34;#django-pitfall-precision-of-decimals-and-duplicate-prevention&#34;&gt;Django pitfall: precision of decimals and duplicate prevention&lt;/a&gt;&lt;/li&gt;      &lt;/ul&gt;    &lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#references--resources&#34;&gt;References &amp;amp; Resources&lt;/a&gt;&lt;/li&gt;  &lt;/ul&gt;&lt;/nav&gt;&lt;/div&gt;&lt;p&gt;In programming we use numbers a lot. For counting integers are quite nice, but for many use cases we need &lt;a href=&#34;https://en.wikipedia.org/wiki/Real_number&#34;&gt;“real” numbers&lt;/a&gt; which apart from integers also include numbers like 1/3, $ \sqrt{2} $, etc. Typically these are approximated with floating point numbers based on base 2. Traditionally we call them &lt;code&gt;floats&lt;/code&gt; .&lt;/p&gt;&lt;p&gt;&lt;code&gt;floats&lt;/code&gt; are great for many use cases, but they have their &lt;a href=&#34;https://docs.python.org/3/tutorial/floatingpoint.html&#34;&gt;issues&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&#34;why-use-decimals&#34;&gt;Why use decimals?&lt;/h2&gt;&lt;p&gt;One big issue is that &lt;code&gt;floats&lt;/code&gt; can&amp;rsquo;t express many of real numbers that are commonly used by humans in daily life without some approximation.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Unfortunately, most decimal fractions cannot be represented exactly as binary fractions. A consequence is that, in general, the decimal floating-point numbers you enter are only approximated by the binary floating-point numbers actually stored in the machine.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Here is an example:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.1&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.1&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.1&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.3&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;False&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That applies to other types of fractions as well, like &lt;code&gt;2/7&lt;/code&gt; or &lt;code&gt;1/3&lt;/code&gt; which also can&amp;rsquo;t be represented accurately.&lt;/p&gt;&lt;p&gt;Some numbers can&amp;rsquo;t be accurately expressed as fractions like square root of  $ \sqrt{2} $  or $\pi$, but for the numbers that can (rational numbers), python has a solution!&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&#34;https://docs.python.org/3/library/fractions.html&#34;&gt;fractions module&lt;/a&gt; - will help you deal with any type of fractional numbers&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://docs.python.org/3/library/decimal.html&#34;&gt;decimal module&lt;/a&gt; - is specialized with decimal numbers like &lt;code&gt;0.12&lt;/code&gt; or &lt;code&gt;99999.999877&lt;/code&gt; which is very useful in business and finance as this is what humans use for money.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This article will focus on the use cases that require decimal arithmetic (e.g. anything related to money) and therefore the use of &lt;code&gt;decimals&lt;/code&gt; .&lt;/p&gt;&lt;h2 id=&#34;decimals-in-action&#34;&gt;Decimals in action&lt;/h2&gt;&lt;p&gt;Python&amp;rsquo;s standard library has a &lt;a href=&#34;https://docs.python.org/3/library/decimal.html&#34;&gt;decimal module&lt;/a&gt;, similarlyit&amp;rsquo;s very well supported in Django and majority of the SQL databases.&lt;/p&gt;&lt;h3 id=&#34;decimals-in-python&#34;&gt;Decimals in python&lt;/h3&gt;&lt;p&gt;To use decimal numbers in python you need to import the &lt;code&gt;decimal&lt;/code&gt; module and create your decimals with &lt;code&gt;decimal.Decimal&lt;/code&gt;&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; decimal&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; decimal&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Decimal(&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; decimal&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Decimal(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;0.75&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Decimal(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;1.50&amp;#39;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When you create a decimal number, you should do it from either an integer or a string (or a specially formatted tuple, but that&amp;rsquo;s more complex). Creating a decimal from a float also works, but there are problems associated with that and I will explain it in the pitfalls section.&lt;/p&gt;&lt;p&gt;Python has a feature called operator overloading and thanks to that you can do normal arithmetic on your decimal numbers without a need to call methods, just with &lt;code&gt;+&lt;/code&gt; , &lt;code&gt;*&lt;/code&gt; etc.&lt;/p&gt;&lt;p&gt;You &lt;code&gt;decimal.Decimal&lt;/code&gt; instances also have various methods, the most useful of which is&lt;a href=&#34;https://docs.python.org/3/library/decimal.html#decimal&#34;&gt;quantize&lt;/a&gt;.&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; Decimal(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;7.325&amp;#39;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;quantize(Decimal(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;.01&amp;#39;&lt;/span&gt;), rounding&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;ROUND_DOWN)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Decimal(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;7.32&amp;#39;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Decimal operations in python are configurable. You can easily change how many digits after full numbers you want and what will happen if you need some rounding or if your arithmetic is imprecise.&lt;/p&gt;&lt;p&gt;Luckily you don&amp;rsquo;t have to configure this per each decimal. Decimal instance, that would be very tedious! Python&amp;rsquo;s &lt;code&gt;decimal&lt;/code&gt; module has a concept of a context where all the possible settingslive and can be overridden.&lt;/p&gt;&lt;p&gt;There is a default global context that you can access and change, or you can use a local context that will be restored after you exit a code block.&lt;/p&gt;&lt;p&gt;Here are two examples of making the same change to the context, global one:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; decimal &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; getcontext&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;getcontext()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;prec &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;42&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Decimal(&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sqrt()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Local one:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; decimal &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; localcontext&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; localcontext() &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; ctx:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;prec &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;42&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;# Perform a high precision calculation&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    s &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; calculate_something()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Contexts are pretty neat and they can allow you to configure &lt;a href=&#34;https://docs.python.org/3/library/decimal.html#decimal.Context&#34;&gt;a lot of things&lt;/a&gt;, like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;how to round&lt;/li&gt;&lt;li&gt;how much precision to use&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;code&gt;decimal&lt;/code&gt; module also supports &lt;a href=&#34;https://docs.python.org/3/library/decimal.html#signals&#34;&gt;Signals&lt;/a&gt; which represent conditions that arise during computations, like &amp;ldquo;Inexact&amp;rdquo; or &amp;ldquo;Overflow&amp;rdquo;.These can be configured to raise exceptions through the decimal context or just simply accessed and cleared.&lt;/p&gt;&lt;h3 id=&#34;decimals-in-django&#34;&gt;Decimals in Django&lt;/h3&gt;&lt;p&gt;Django supports decimals through a special &lt;a href=&#34;https://docs.djangoproject.com/en/4.0/ref/models/fields/#decimalfield&#34;&gt;models.DecimalField&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;You can configure the amount of digits and decimal places. The decimal places are included in the digits.Example model with a precise decimal:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PriceHistory&lt;/span&gt;(models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Model):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    asset &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ForeignKey(Asset, on_delete&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CASCADE)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    value &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;DecimalField(max_digits&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;, decimal_places&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    date &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;DateField()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Similarly, it&amp;rsquo;s supported in Django forms and &lt;a href=&#34;https://www.django-rest-framework.org/api-guide/fields/#decimalfield&#34;&gt;django rest framework&lt;/a&gt;.&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PositionSerializer&lt;/span&gt;(serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ModelSerializer[Position]):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    asset &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; AssetSerializer()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# Latest price is not defined on the model directly.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    latest_price &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;DecimalField(max_digits&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;, decimal_places&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Meta&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        model &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Position&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        fields &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;account&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;asset&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;quantity&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;latest_price&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;latest_price_date&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;latest_exchange_rate&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;realized_gain&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;cost_basis&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There is no need to specify &lt;code&gt;max_digits&lt;/code&gt; and &lt;code&gt;decimal_places&lt;/code&gt; for a field that is both in the model and in the serializer.&lt;/p&gt;&lt;h3 id=&#34;decimals-in-the-sql-database&#34;&gt;Decimals in the SQL database&lt;/h3&gt;&lt;p&gt;Decimals are well supported by majority of SQL databases (&lt;a href=&#34;https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL&#34;&gt;PostgreSQL&lt;/a&gt; and &lt;a href=&#34;https://dev.mysql.com/doc/refman/8.0/en/fixed-point-types.html&#34;&gt;MySQL&lt;/a&gt;) as it&amp;rsquo;s part of the SQL standard.&lt;/p&gt;&lt;p&gt;Note: SQLite doesn&amp;rsquo;t have support for actual decimals. So things might work in unexpected ways.&lt;/p&gt;&lt;p&gt;The implementation between Databases might vary, for example PostgreSQL supports many more significant digits than MySQL (by multiple orders of magnitude).&lt;/p&gt;&lt;p&gt;The decimal SQL standard requires &lt;code&gt;precision&lt;/code&gt; and &lt;code&gt;scale&lt;/code&gt; to be configured when the decimal column is added. The meaning of those parameters is equivalent to Django&amp;rsquo;s &lt;code&gt;max_digits&lt;/code&gt; and &lt;code&gt;decimal_places&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;So what can this precision cost us? More digits incurs storage overhead.&lt;/p&gt;&lt;p&gt;From PostgreSQL documentation:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Numeric values are physically stored without any extra leading or trailing zeroes. Thus, the declared precision and scale of a column are maximums, not fixed allocations. (In this sense the  &lt;code&gt;numeric&lt;/code&gt;  type is more akin to  &lt;code&gt;varchar(***n***)&lt;/code&gt;  than to  &lt;code&gt;char(***n***)&lt;/code&gt; .) The actual storage requirement is two bytes for each group of four decimal digits, plus three to eight bytes overhead.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;So my Django decimal with 20 max digits and 10 decimal places will require 2 * 5 + 8 (worst case overhead), 18 bytes per number!&lt;/p&gt;&lt;p&gt;A &lt;code&gt;double precision&lt;/code&gt; number would be 8 bytes.&lt;/p&gt;&lt;p&gt;That difference can add up pretty quickly.&lt;/p&gt;&lt;h2 id=&#34;downsides-and-pitfalls&#34;&gt;Downsides and pitfalls&lt;/h2&gt;&lt;p&gt;Now that you are convinced that decimals are often necessary and you have learned how they work, let&amp;rsquo;s talk about how you canshoot yourself in your foot using them.&lt;/p&gt;&lt;h3 id=&#34;mixing-floats-and-decimals&#34;&gt;Mixing floats and decimals&lt;/h3&gt;&lt;p&gt;Decimal-float operations are not supported for a good reason.&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt; decimal&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Decimal(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;0.3&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.8&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;TypeError&lt;/span&gt;                                 Traceback (most recent call last)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;ipython&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;input&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;6&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;92&lt;/span&gt;fb256311c2&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;module&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;----&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; decimal&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Decimal(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;0.3&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.8&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;TypeError&lt;/span&gt;: unsupported operand type(s) &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;decimal.Decimal&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;float&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the case above &lt;code&gt;0.8&lt;/code&gt; is not exactly &lt;code&gt;decimal.Decimal(&amp;quot;0.8&amp;quot;)&lt;/code&gt; it would have to be convertedto a decimal and that could result in some imprecision. If you hit this type of errors, it meansthat some of your numbers are not decimals and they can make the result of your computationsnot exact in the end.&lt;/p&gt;&lt;p&gt;So what to do? Just convert the float to decimal, by using &lt;code&gt;decimal.Decimal(my_float_number)&lt;/code&gt;? Well, no&amp;hellip;&lt;/p&gt;&lt;h3 id=&#34;if-you-create-decimals-from-floats-you-are-loosing-accuracy&#34;&gt;If you create decimals from floats you are loosing accuracy&lt;/h3&gt;&lt;p&gt;This another pitfall, as every time you create decimals from floats, your number get imprecise.&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; decimal &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Decimal&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; Decimal(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;0.1&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; Decimal(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;0.1&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; Decimal(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;0.1&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; Decimal(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;0.3&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; Decimal(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.1&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; Decimal(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.1&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; Decimal(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.1&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; Decimal(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.3&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;False&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Instead, keep the numbers in your system as strings, integers or decimals without ever converting them to floats.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;TIP: use &lt;a href=&#34;https://www.ralphminderhoud.com/blog/django-mypy-check-runs/&#34;&gt;type annotations&lt;/a&gt; to annotate all decimals.&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&#34;default-context-vs-local-context&#34;&gt;Default context vs local context&lt;/h3&gt;&lt;p&gt;I would generally advise against modifying a global context in decimals in your code, unless you have a simple program and you clearly do it in the beginning of its lifetime.&lt;/p&gt;&lt;p&gt;If you modify it at random points, you might end up with unexpected values in your context if you forget to clean them up!&lt;/p&gt;&lt;p&gt;A much better solution is to always keep it local with &lt;code&gt;localcontext&lt;/code&gt;.&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; decimal &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; localcontext&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; localcontext() &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; ctx:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;prec &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;42&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;# Perform a high precision calculation&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    s &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; calculate_something()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You might even want to declare your custom decorator for that.&lt;/p&gt;&lt;h3 id=&#34;django-pitfall-formsserializers-out-of-sync-with-model-field-definition&#34;&gt;Django pitfall: forms/serializers out of sync with model field definition&lt;/h3&gt;&lt;p&gt;It&amp;rsquo;s not only that you don&amp;rsquo;t need to specify &lt;code&gt;max_digits&lt;/code&gt; and &lt;code&gt;decimal_places&lt;/code&gt; on a matching field between model and serializer, you shouldn&amp;rsquo;t.&lt;/p&gt;&lt;p&gt;Why? Any time they get out of sync it can lead to problems, one type if it&amp;rsquo;s bigger on a serializer and one type if it&amp;rsquo;s smaller than on the model.&lt;/p&gt;&lt;p&gt;When serializer allows for more digits then user will eventually put more digits there. Their input will pass the validation but then fail at the database level raising an exception and failing to the user.&lt;/p&gt;&lt;p&gt;When serializer allows for less digits than the model field, then e.g. when the value is edited by the user it might get truncated even if user hasn&amp;rsquo;t really made any changes to the value. This can happen if the user changed other values, but the API is passing all the values again.&lt;/p&gt;&lt;p&gt;Both errors to the users and unexpected losses of precisions are pretty bad, so pay attention to this as it might be hard to catch in tests.&lt;/p&gt;&lt;h3 id=&#34;django-pitfall-precision-of-decimals-and-duplicate-prevention&#34;&gt;Django pitfall: precision of decimals and duplicate prevention&lt;/h3&gt;&lt;p&gt;I&amp;rsquo;m a big fan of using &lt;a href=&#34;https://docs.djangoproject.com/en/4.0/ref/models/querysets/#get-or-create&#34;&gt;&lt;code&gt;get_or_create&lt;/code&gt;&lt;/a&gt; in Django to prevent duplicates when the value is created with the same arguments.&lt;/p&gt;&lt;p&gt;But the arguments won&amp;rsquo;t be the same if the decimal&amp;rsquo;s precision differs!&lt;/p&gt;&lt;p&gt;So be careful with that as &lt;code&gt;decimal&lt;/code&gt; module precision defaults to &lt;code&gt;28&lt;/code&gt; places and that is very likely not what you have configured on you model.&lt;/p&gt;&lt;p&gt;To deal with that you can override your context to use required precision when performing arithmetic, or even better you can use the &lt;code&gt;quantize&lt;/code&gt; method on the value.&lt;/p&gt;&lt;h2 id=&#34;references--resources&#34;&gt;References &amp;amp; Resources&lt;/h2&gt;&lt;p&gt;I hope you enjoyed this long post! Please follow me on &lt;a href=&#34;https://twitter.com/attilczuk&#34;&gt;twitter&lt;/a&gt; and share it if you found it useful!&lt;/p&gt;&lt;p&gt;Additional resources:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&#34;https://docs.python.org/3/library/decimal.html&#34;&gt;Official decimal docs&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://pymotw.com/3/decimal/index.html&#34;&gt;PyMoTW&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://dev.mysql.com/doc/refman/8.0/en/fixed-point-types.html&#34;&gt;MySQL&lt;/a&gt; and &lt;a href=&#34;https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL&#34;&gt;PostgreSQL&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://docs.python.org/3/tutorial/floatingpoint.html&#34;&gt;Floating points&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://docs.djangoproject.com/en/4.0/ref/models/fields/#decimalfield&#34;&gt;Django&amp;rsquo;s DecimalField&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</description>
     </item>
   
     <item>
       <title>Invertimo Binance Crypto Integration</title>
       <link>https://tinystruggles.com/posts/invertimo_binance_crypto_integration/</link>
       <pubDate>Wed, 02 Feb 2022 13:15:59 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/invertimo_binance_crypto_integration/</guid>
       <description>&lt;p&gt;You can now import your &lt;a href=&#34;https://binance.com&#34;&gt;binance&lt;/a&gt; Crypto related transactions to &lt;a href=&#34;https://invertimo.com&#34;&gt;invertimo.com&lt;/a&gt; (&lt;a href=&#34;https://tinystruggles.com/posts/building_invertimo_in_the_open/&#34;&gt;open source investment tracking app&lt;/a&gt;).&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/binance-logo.png&#34; alt=&#34;binance logo&#34;&gt;&lt;/p&gt;&lt;h2 id=&#34;why-track-crypto-transactions-in-invertimo&#34;&gt;Why track crypto transactions in invertimo?&lt;/h2&gt;&lt;p&gt;Invertimo helps you track all transactions related to your investments as well as dividends and received income in one place.Crypto is becoming more and more mainstream and more investors have part of their portfolio in crypto assets.&lt;/p&gt;&lt;p&gt;Selling crypto is a taxable event in most countries, swapping tokens is usually treated similarly.&lt;/p&gt;&lt;p&gt;Because of that you need to be on top of your transactions and know what was your costbasis (total amount paid) of the tokens you sold. You need to make sure you track it in the correct currency,converting USD gain to your local currency is not the way to do it. Additionally, order of selling matters as well,even if you have the same token in multiple wallets or exchanges! Do yourself a favor and keep a solid record of your transactions!&lt;/p&gt;&lt;p&gt;Crypto can be an income producing asset and you need to track this income too. The income is usually produced by Savings products (similar to bank savings accounts) or through staking (helping secure the blockchain). Tracking the income can be especially challenging and you can read more about it in a section later on.&lt;/p&gt;&lt;h2 id=&#34;why-binance&#34;&gt;Why binance?&lt;/h2&gt;&lt;p&gt;&lt;a href=&#34;https://binance.com&#34;&gt;Binance&lt;/a&gt; is one of the biggest centralized exchange through which many people buy crypto.&lt;/p&gt;&lt;p&gt;It&amp;rsquo;s also the exchange I&amp;rsquo;ve been using personally, so I prioritized building the integration as I could be my own test rabbit.&lt;/p&gt;&lt;h2 id=&#34;crypto-income--problem-with-daily-interest&#34;&gt;Crypto Income &amp;amp; problem with daily interest&lt;/h2&gt;&lt;p&gt;When you receive interest from your tokens two things happen:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;you receive income of the value of the tokens you get&lt;/li&gt;&lt;li&gt;you receive the tokens with a cost basis of the income you just received&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Let&amp;rsquo;s see it in an example, let&amp;rsquo;s say you stake a token DOT. It has a high interest rate and produces income daily.&lt;/p&gt;&lt;p&gt;On Monday you receive 0.02 DOT. The price per token is ~20 USD on that day. You are based in Europe and taxed in euros.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;You received 0.2 USD of income! 1 USD is 0.88 EUR at that day, so you had an income of 0.352 EUR. You don&amp;rsquo;t see it in your account balance, but instead:&lt;/li&gt;&lt;li&gt;you have additional 0.02 DOT that if you sold next week you would have to pay capital gains on. Their cost basis was 0.352 EUR.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So that was for Monday, a couple days later the price might be completely different as crypto assets are quite volatile.&lt;img src=&#34;https://tinystruggles.com/polkadot_price.png&#34; alt=&#34;polkadot price&#34;&gt;&lt;/p&gt;&lt;p&gt;And guess what, currency exchange rates fluctuate a lot too!&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/currency.png&#34; alt=&#34;euro price&#34;&gt;&lt;/p&gt;&lt;p&gt;And if you own multiple different tokens that have such daily interest, the amount of interest events and transactions quickly adds up!It can reach thousands in no time.&lt;/p&gt;&lt;p&gt;It happened to me in a couple of months.&lt;img src=&#34;https://tinystruggles.com/transactions_crypto.png&#34; alt=&#34;transactions add up&#34;&gt;&lt;/p&gt;&lt;h2 id=&#34;data-sources-in-invertimo&#34;&gt;Data sources in invertimo&lt;/h2&gt;&lt;p&gt;To get the crypto prices and currency exchange rates, you need a reliable data source. Invertimo gets its market data from&lt;a href=&#34;https://eodhistoricaldata.com/&#34;&gt;eodhistoricaldata&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you trade tokens for fiat currency, the app uses how much fiat you paid as a price used in that specific transaction.&lt;/p&gt;&lt;h2 id=&#34;how-it-works&#34;&gt;How it works&lt;/h2&gt;&lt;h3 id=&#34;csv-import&#34;&gt;CSV import&lt;/h3&gt;&lt;p&gt;Once you export your binance transactions (&lt;a href=&#34;https://www.binance.com/en/support/faq/990afa0a0a9341f78e7a9298a9575163&#34;&gt;instructions here&lt;/a&gt;), youcan upload a &lt;code&gt;.csv&lt;/code&gt; file to invertimo.&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/import.png&#34; alt=&#34;import&#34;&gt;&lt;/p&gt;&lt;p&gt;The binance parser under the hood will create all necessary transaction records and events.&lt;img src=&#34;https://tinystruggles.com/successful_import.png&#34; alt=&#34;success&#34;&gt;&lt;/p&gt;&lt;h3 id=&#34;transparency&#34;&gt;Transparency&lt;/h3&gt;&lt;p&gt;You can see exactly what happened in the given import. The import can be successful, partially successful or fail completely (unlikely!).&lt;/p&gt;&lt;p&gt;Any transaction or account event that was created as a result of an import has an associated import record that stores information used for the creation of that transaction or event so you can inspect it.&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/crypto_staking_transparency.png&#34; alt=&#34;transparency&#34;&gt;&lt;/p&gt;&lt;h3 id=&#34;deduplication&#34;&gt;Deduplication&lt;/h3&gt;&lt;p&gt;One of the design principles of &lt;a href=&#34;https://invertimo.com&#34;&gt;invertimo.com&lt;/a&gt; is that it prevents duplicate records. So there is no need to worry that something will be recordedmultiple times even if you upload history files with overlapping transactions.&lt;/p&gt;&lt;p&gt;We will store in the database that the record was imported, but deduplicated.&lt;/p&gt;&lt;h2 id=&#34;current-limitations&#34;&gt;Current limitations&lt;/h2&gt;&lt;p&gt;Only SPOT account transactions are supported and only the following binance operations:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;BINANCE_SUPPORTED_OPERATIONS &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;POS savings interest&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Savings Interest&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ETH 2.0 Staking Rewards&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;POS savings redemption&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;POS savings purchase&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Savings Principal redemption&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Savings purchase&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Deposit&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Transaction Related&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ETH 2.0 Staking&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Withdrawal&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In other words:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;transferring money in and out of binance&lt;/li&gt;&lt;li&gt;buying or selling crypto&lt;/li&gt;&lt;li&gt;staking interest&lt;/li&gt;&lt;li&gt;savings interest&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I&amp;rsquo;m not sure how exhaustive this set is, but if you have more I should support, I would be happy to add it.&lt;/p&gt;&lt;h2 id=&#34;alternatives&#34;&gt;Alternatives&lt;/h2&gt;&lt;p&gt;If you want a solution just for crypto taxes, there exist some solid alternatives!In particular I have been using &lt;a href=&#34;https://koinly.io&#34;&gt;koinly.io&lt;/a&gt; so far!&lt;/p&gt;&lt;p&gt;The problem is that it &lt;a href=&#34;https://koinly.io/pricing/&#34;&gt;charges a lot&lt;/a&gt; if you have crypto income:&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/koinly_pricing.png&#34; alt=&#34;koinly pricing&#34;&gt;&lt;/p&gt;&lt;p&gt;With my couple months of transactions, I would have to pay them almost 200 USD per year.&lt;/p&gt;&lt;p&gt;Invertimo is not perfect (under active development!), but it is &lt;a href=&#34;https://github.com/ilonajulczuk/invertimo&#34;&gt;open source&lt;/a&gt; and even though it won&amp;rsquo;t compute the taxes for you,it will help you track the transactions and income and that is more than a half of the battle.&lt;/p&gt;&lt;h2 id=&#34;what-next&#34;&gt;What next?&lt;/h2&gt;&lt;p&gt;The crypto support in invertimo is far from done. I am planning more UX improvements and better support for crypto transactions outside of the binance integration.&lt;/p&gt;&lt;p&gt;The journey is never over! If you enjoy this article, please follow me on &lt;a href=&#34;https://twitter.com/attilczuk&#34;&gt;twitter&lt;/a&gt;.&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Browser caching with Django &amp; Webpack</title>
       <link>https://tinystruggles.com/posts/browser_caching_django_webpack/</link>
       <pubDate>Sat, 08 Jan 2022 10:45:59 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/browser_caching_django_webpack/</guid>
       <description>&lt;div class=&#34;toc&#34;&gt;    &lt;h3&gt;Table of contents&lt;/h3&gt;    &lt;nav id=&#34;TableOfContents&#34;&gt;  &lt;ul&gt;    &lt;li&gt;&lt;a href=&#34;#what-is-browser-caching-and-why-is-it-useful&#34;&gt;What is browser caching and why is it useful?&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#crude-approaches&#34;&gt;Crude approaches&lt;/a&gt;      &lt;ul&gt;        &lt;li&gt;&lt;a href=&#34;#disable-all-caching&#34;&gt;Disable all caching!&lt;/a&gt;&lt;/li&gt;        &lt;li&gt;&lt;a href=&#34;#change-the-filenameimport-every-time-you-edit&#34;&gt;Change the filename/import every time you edit&lt;/a&gt;&lt;/li&gt;      &lt;/ul&gt;    &lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#content-hashes-in-filenames&#34;&gt;Content hashes in filenames&lt;/a&gt;      &lt;ul&gt;        &lt;li&gt;&lt;a href=&#34;#let-django-do-it-for-you&#34;&gt;Let Django do it for you&lt;/a&gt;&lt;/li&gt;        &lt;li&gt;&lt;a href=&#34;#let-webpack-do-it-for-you&#34;&gt;Let webpack do it for you&lt;/a&gt;&lt;/li&gt;        &lt;li&gt;&lt;a href=&#34;#webpack--django-integration-without-any-additional-packages&#34;&gt;Webpack &amp;amp; Django integration without any additional packages&lt;/a&gt;&lt;/li&gt;        &lt;li&gt;&lt;a href=&#34;#combine-django-and-webpack-file-caching-management&#34;&gt;Combine Django and webpack file caching management&lt;/a&gt;&lt;/li&gt;      &lt;/ul&gt;    &lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#conclusion--resources&#34;&gt;Conclusion &amp;amp; resources&lt;/a&gt;&lt;/li&gt;  &lt;/ul&gt;&lt;/nav&gt;&lt;/div&gt;&lt;h2 id=&#34;what-is-browser-caching-and-why-is-it-useful&#34;&gt;What is browser caching and why is it useful?&lt;/h2&gt;&lt;p&gt;Fetching stuff from the internet can be a lot of work and take a long time. What if your browsercould save itself all this work and return you the result semi-instantly?&lt;/p&gt;&lt;p&gt;It just needs to save a file locally and return it to you next time you want it. We call it &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching&#34;&gt;browser HTTP caching&lt;/a&gt;.Passive operations like &amp;ldquo;getting&amp;rdquo; the page will usually be cached.&lt;/p&gt;&lt;p&gt;This is great as long as the file at this address doesn&amp;rsquo;t change. But the thing is that it often does, especially if your site is under active development.&lt;/p&gt;&lt;p&gt;When things are just not working the way they are supposed to or styles are off, it&amp;rsquo;s very likelythat the problem is unintended caching.&lt;/p&gt;&lt;h2 id=&#34;crude-approaches&#34;&gt;Crude approaches&lt;/h2&gt;&lt;p&gt;Two popular crude approaches are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;disabling caching&lt;/li&gt;&lt;li&gt;manually managing the filenames after edit&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;disable-all-caching&#34;&gt;Disable all caching!&lt;/h3&gt;&lt;p&gt;One simple, but crude approach is to disable caching.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;If caching is the source of my problem, let&amp;rsquo;s disable it!&lt;/p&gt;&lt;/blockquote&gt;&lt;h4 id=&#34;in-your-browser&#34;&gt;In your browser&lt;/h4&gt;&lt;p&gt;This is a good approach if you are not sure what is your problem and want to quicklyverify if caching is to be blamed.&lt;/p&gt;&lt;p&gt;Modern browsers have an option to disable cache in the &lt;a href=&#34;https://developer.chrome.com/docs/devtools/&#34;&gt;developer tools&lt;/a&gt;:&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/chrome_devtools_disable_cache.png&#34; alt=&#34;Disable cache&#34;&gt;&lt;/p&gt;&lt;p&gt;After you disable caching in the options, reload the page to get new content.&lt;/p&gt;&lt;p&gt;This is one of the &amp;ldquo;works on my machine&amp;rdquo; 🤦 types of solutions and you can&amp;rsquo;t expectyour users to clear or disable their cache just because your app doesn&amp;rsquo;t handle caching well.&lt;/p&gt;&lt;p&gt;We need something better!&lt;/p&gt;&lt;h4 id=&#34;on-the-server-side&#34;&gt;On the server side&lt;/h4&gt;&lt;p&gt;The good news is that the browser will do what you tell it to do. You can &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#controlling_caching&#34;&gt;set certain response headers&lt;/a&gt; ( &lt;code&gt;Cache-Control&lt;/code&gt; ) tocontrol the browser cache behavior when you send back responses from your server.&lt;/p&gt;&lt;p&gt;If you use nginx (a web proxy server often used with Django), you can disable &lt;a href=&#34;https://stackoverflow.com/questions/40243633/disable-nginx-cache-for-javascript-files&#34;&gt;caching there&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you use the Django&amp;rsquo;s static convention, nginx config that disable caching would be:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-nginx&#34; data-lang=&#34;nginx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;server&lt;/span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;location&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;/static&lt;/span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;alias&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;/var/www/mysite.com/static&lt;/span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# kill cache&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;add_header&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;Last-Modified&lt;/span&gt; $date_gmt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;add_header&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;Cache-Control&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;no-store,&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;no-cache&amp;#39;&lt;/span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;if_modified_since&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;off&lt;/span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;expires&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;off&lt;/span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;etag&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;off&lt;/span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This approach works, but it has a disadvantage. Files that don&amp;rsquo;t change and could be cached, don&amp;rsquo;t get cached and you are missing outon site speed ups.&lt;/p&gt;&lt;h3 id=&#34;change-the-filenameimport-every-time-you-edit&#34;&gt;Change the filename/import every time you edit&lt;/h3&gt;&lt;p&gt;If files get cached if the URL is the same, why not change the URL if the file changes?&lt;/p&gt;&lt;p&gt;Renaming the file every time you edit would betedious and in general, we have version control so we don&amp;rsquo;t have to create a new file every time we make changes.&lt;/p&gt;&lt;p&gt;But there exist a related technique of changing the URL by adding &amp;ldquo;?version=XXX&amp;rdquo; a versioning query parameter.If you are just serving static files, without any custom logic, the additional parameters are ignored by your server,but the browser will treat it as a new URL.&lt;/p&gt;&lt;p&gt;So instead of:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;script&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;src&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/static/my_script.js&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;script&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;link&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;rel&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;href&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/static/base.css&amp;#34;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;you would write:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;script&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;src&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/static/my_script.js?version=123&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;script&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;link&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;rel&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;href&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/static/base.css?version=123&amp;#34;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and in case of django with the &lt;code&gt;static&lt;/code&gt; template tag:&lt;/p&gt;&lt;p&gt;Instead of:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;link&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;rel&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;href&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;{% static &amp;#39;base.css&amp;#39; %}&amp;#34;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;you would write:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;link&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;rel&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;href&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;{% static &amp;#39;base.css&amp;#39; %}?version=123&amp;#34;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With renaming you get a finer control than just disabling cache, because the things that can be cached stay cached.But it&amp;rsquo;s still not a great solution.&lt;/p&gt;&lt;p&gt;Using a version parameter in the URL is a better approach than just renaming the filename every time you make the edits yourself, but it&amp;rsquo;s still manual and error prone.It&amp;rsquo;s easy to forget to do it. Why not automate it?&lt;/p&gt;&lt;h2 id=&#34;content-hashes-in-filenames&#34;&gt;Content hashes in filenames&lt;/h2&gt;&lt;p&gt;One good way of updating the file name any time the file changes is by usinghashes of the content as part of a file name.&lt;/p&gt;&lt;p&gt;Doing this all the time, would be a bit expensive, so it&amp;rsquo;s usually done during a build process.&lt;/p&gt;&lt;p&gt;If you are using Django together with React (or Vue, or&amp;hellip;) in a way that Django serves page that thenloads a JavaScript bundle built with &lt;a href=&#34;https://webpack.js.org/&#34;&gt;webpack&lt;/a&gt; then you build has two phases:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&#34;https://webpack.js.org/&#34;&gt;webpack&lt;/a&gt; build to build your JS assets&lt;/li&gt;&lt;li&gt;Django &lt;a href=&#34;https://docs.djangoproject.com/en/4.0/howto/static-files/&#34;&gt;collectstatic&lt;/a&gt; to do any preprocessing and move files to the &lt;code&gt;static&lt;/code&gt; location.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Typically the way it&amp;rsquo;s set up is that your JS code is in some directory, e.g. called &lt;code&gt;assets&lt;/code&gt; and when your JavaScript bundle is compiled thebundle is put into &lt;code&gt;/static&lt;/code&gt; directory, the default location for Django&amp;rsquo;s JS and CSS files.&lt;/p&gt;&lt;p&gt;You can read about how to set up Django with JS like this &lt;a href=&#34;https://www.saaspegasus.com/guides/modern-javascript-for-django-developers/&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;h3 id=&#34;let-django-do-it-for-you&#34;&gt;Let Django do it for you&lt;/h3&gt;&lt;p&gt;Turns out Django has a way to automatically add hashes to your static files any time the content changes.&lt;/p&gt;&lt;p&gt;Just update your staticfiles storage implementation to &lt;a href=&#34;https://docs.djangoproject.com/en/3.2/ref/contrib/staticfiles/#manifeststaticfilesstorage&#34;&gt; &lt;code&gt;ManifestStaticFilesStorage&lt;/code&gt; &lt;/a&gt; in your Django &lt;code&gt;settings.py&lt;/code&gt; file.&lt;code&gt;ManifestStaticFilesStorage&lt;/code&gt; computes the hashes based on file&amp;rsquo;s content, adds the hashes to the file names and then updates all the import paths that use &lt;code&gt;static&lt;/code&gt; tag appropriately.&lt;/p&gt;&lt;p&gt;Nice!&lt;/p&gt;&lt;p&gt;Well, that works, unless you use code splitting and hashing with webpack. Unless your JS app is really small, you shouldn&amp;rsquo;t have it all in just one file. This is where the trouble starts with using the Django manifest solution.&lt;/p&gt;&lt;h3 id=&#34;let-webpack-do-it-for-you&#34;&gt;Let webpack do it for you&lt;/h3&gt;&lt;p&gt;If you are still reading, you are probably curious how manage things on the JavaScript side if your bundle consists of many files.&lt;/p&gt;&lt;p&gt;Well, webpack has support for &lt;a href=&#34;https://webpack.js.org/guides/caching/&#34;&gt;caching&lt;/a&gt; by generating file names with content hashes too.&lt;/p&gt;&lt;p&gt;But then once you have a bundle with a hash in its name, how to integrate it to work with Django.&lt;/p&gt;&lt;p&gt;Google search suggests to use &lt;a href=&#34;https://github.com/django-webpack/django-webpack-loader&#34;&gt;django-webpack-loader&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/django_webpack_2022.png&#34; alt=&#34;Best practices&#34;&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://www.accordbox.com/blog/load-webpack-hash-bundle-django/&#34;&gt;This tutorial&lt;/a&gt; talks in detail how to set it up.&lt;/p&gt;&lt;p&gt;This solution requires a custom plugin on the JavaScript side and a custom package and app on the Django side.&lt;/p&gt;&lt;p&gt;Quite a setup to just load a bunch of correct files. I&amp;rsquo;m always skeptical when it comes to taking on new dependencies.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;django-webpack-loader&lt;/code&gt; solution is not native to webpack and it &lt;a href=&#34;https://github.com/django-webpack/django-webpack-loader/issues/157&#34;&gt;broke in 2018&lt;/a&gt; when webpackchanged some of its defaults. &lt;code&gt;django-webpack-loader&lt;/code&gt; solution is rather fragile, it depends on reading webpack stats that are a byproduct of a webpack build (if configured).&lt;/p&gt;&lt;p&gt;While reading the Webpack &lt;a href=&#34;https://webpack.js.org/guides/caching/&#34;&gt;caching&lt;/a&gt; documentation I came up with a simpler solution that doesn&amp;rsquo;t require any new dependencies.&lt;/p&gt;&lt;h3 id=&#34;webpack--django-integration-without-any-additional-packages&#34;&gt;Webpack &amp;amp; Django integration without any additional packages&lt;/h3&gt;&lt;p&gt;Loading a bundle with a hash in a name is a common problem, not specific to Django. And webpack now has a native solution tosolve it. It does that by &lt;a href=&#34;https://webpack.js.org/guides/caching/#output-filenames&#34;&gt;generating a html file&lt;/a&gt; that will load your bundle.&lt;/p&gt;&lt;p&gt;A html file, just like the Django templates&amp;hellip;&lt;/p&gt;&lt;p&gt;Can webpack generate a Django template? Of course it can! If you don&amp;rsquo;t need anything Django specific then you don&amp;rsquo;t even have tocustomize it, but if you do, it&amp;rsquo;s easy to specify a &amp;ldquo;template&amp;rdquo; file for webpack to use.&lt;/p&gt;&lt;p&gt;Here is how it can be set up:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-JavaScript&#34; data-lang=&#34;JavaScript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;path&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;require&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;path&amp;#39;&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;HtmlWebpackPlugin&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;require&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;html-webpack-plugin&amp;#39;&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;module&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;exports&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;plugins&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;HtmlWebpackPlugin&lt;/span&gt;({&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;// This is the template webpack will use to generate html file.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;template&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;templates/index.tmpl.html&amp;#39;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;// This is where the generated file will end up (relative to the `static` directory).&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;filename&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;../templates/index.webpack.html&amp;#39;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        })&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ],&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;entry&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;./assets/index.js&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#75715e&#34;&gt;// Path to our input file.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;output&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;filename&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;[name].[contenthash].index-bundle.js&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#75715e&#34;&gt;// Output bundle file name.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;path&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;path&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;resolve&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;__dirname&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;./static&amp;#39;&lt;/span&gt;), &lt;span style=&#34;color:#75715e&#34;&gt;// Path to our Django `static` directory.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;    },&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// Code Splitting and bundle optimizations.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;optimization&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;usedExports&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;moduleIds&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;deterministic&amp;#39;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;runtimeChunk&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;single&amp;#39;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;splitChunks&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#a6e22e&#34;&gt;cacheGroups&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#a6e22e&#34;&gt;vendor&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#a6e22e&#34;&gt;test&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;/[\\/]node_modules[\\/]/&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#a6e22e&#34;&gt;name&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;vendors&amp;#39;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#a6e22e&#34;&gt;chunks&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;all&amp;#39;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                },&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            },&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        },&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// Other webpack configuration...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Contents of &lt;code&gt;templates/index.tmpl.html&lt;/code&gt;&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{% load static %}&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;html&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;lang&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;en&amp;#34;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;head&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;title&lt;/span&gt;&amp;gt;My page&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;title&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;!-- Webpack imports with auto generated bundle names that avoid caching if data changes. --&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;&amp;lt;!-- My other things... --&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {% block more_head %}&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {% endblock %}&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;head&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;body&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {% block content %}&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {% endblock %}&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;body&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;html&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now when webpack builds your JavaScript assets it will place its outputs in the &lt;code&gt;/static&lt;/code&gt; directory and also createan additional file in your &lt;code&gt;templates&lt;/code&gt; directory. In the case of above example it will generate&lt;code&gt;/templates/index.webpack.html&lt;/code&gt; that will load relevant bundle entry points (one or more).&lt;/p&gt;&lt;p&gt;This solution might look a bit complicated, but it&amp;rsquo;s actually pretty straightforward, it just has two build steps and webpackcreates files in two directories, in &lt;code&gt;/static&lt;/code&gt; directory and in &lt;code&gt;/templates&lt;/code&gt; .&lt;/p&gt;&lt;h3 id=&#34;combine-django-and-webpack-file-caching-management&#34;&gt;Combine Django and webpack file caching management&lt;/h3&gt;&lt;p&gt;You might have figured out that if both webpack and Django generate content hashes, you might end up with a lot of hashes and thatmight break lazy module imports in your JavaScript app.&lt;/p&gt;&lt;p&gt;The easiest way to circumvent that is to either stop using the Django solution, or to make it &lt;a href=&#34;https://docs.djangoproject.com/en/4.0/ref/contrib/staticfiles/#collectstatic&#34;&gt;ignore&lt;/a&gt; the webpack generated files.&lt;/p&gt;&lt;p&gt;Probably the simplest way is to change the way you call &lt;code&gt;collectstatic&lt;/code&gt; . As in the webpack configuration in an example aboveall the files generated by webpack have &lt;code&gt;index-bundle.js&lt;/code&gt; suffix:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python manage.py collectstatic --ignore *.index-bundle.js&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;conclusion--resources&#34;&gt;Conclusion &amp;amp; resources&lt;/h2&gt;&lt;p&gt;So now we have multiple solutions for always loading the fresh version of the file when it changes.And all the files that don&amp;rsquo;t change can now be cached for a very long time speeding up our site ✨.&lt;/p&gt;&lt;p&gt;I hope you enjoyed this article, please follow me on &lt;a href=&#34;https://twitter.com/attilczuk&#34;&gt;twitter&lt;/a&gt; where I talk more about tech, python, Django and building software!&lt;/p&gt;&lt;p&gt;If you are hungry for more, here is a list of resources I refer to in this article you can read as well:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#controlling_caching&#34;&gt;https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#controlling_caching&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://calendar.perfplanet.com/2016/a-tale-of-four-caches/&#34;&gt;https://calendar.perfplanet.com/2016/a-tale-of-four-caches/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://developers.google.com/web/fundamentals/performance/get-started/httpcaching-6&#34;&gt;https://developers.google.com/web/fundamentals/performance/get-started/httpcaching-6&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://docs.djangoproject.com/en/4.0/ref/contrib/staticfiles/#manifeststaticfilesstorage&#34;&gt;https://docs.djangoproject.com/en/4.0/ref/contrib/staticfiles/#manifeststaticfilesstorage&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://stackoverflow.com/questions/40243633/disable-nginx-cache-for-javascript-files&#34;&gt;https://stackoverflow.com/questions/40243633/disable-nginx-cache-for-javascript-files&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://blog.xoxzo.com/en/2018/08/22/cache-busting-in-django/&#34;&gt;https://blog.xoxzo.com/en/2018/08/22/cache-busting-in-django/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://www.accordbox.com/blog/load-webpack-hash-bundle-django/&#34;&gt;https://www.accordbox.com/blog/load-webpack-hash-bundle-django/&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</description>
     </item>
   
     <item>
       <title>Building Invertimo in the open (as open source)</title>
       <link>https://tinystruggles.com/posts/building_invertimo_in_the_open/</link>
       <pubDate>Sat, 08 Jan 2022 10:45:59 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/building_invertimo_in_the_open/</guid>
       <description>&lt;p&gt;As far as building in public goes, it’s hard to go more public than making all the code public too! I am building the &lt;a href=&#34;https://github.com/ilonajulczuk/invertimo&#34;&gt;Invertimo&lt;/a&gt; (investment bookkeeping and tracking software) completely in the open. It’s a complex web app written in python and JavaScript.&lt;/p&gt;&lt;div style=&#34;margin-top: 1em; margin-bottom: 1em; display: flex; flex-wrap: wrap; gap: 10px &#34;&gt;&lt;img src=&#34;https://tinystruggles.com/tech_used.png&#34; style=&#34;max-width: 400px&#34;&gt;&lt;img src=&#34;https://tinystruggles.com/invertimo_commit.png&#34; style=&#34;&#34;&gt;&lt;/div&gt;&lt;p&gt;I submitted 200+ commits over last couple months.&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://github.com/ilonajulczuk/invertimo&#34;&gt;Github repo&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&#34;i-got-the-rights&#34;&gt;I got the rights&lt;/h2&gt;&lt;p&gt;I work at a bit tech company that by default owns everything that I build. There is a process though to ask for permissions to retain the copyrights as a creator. If I have the rights, my company doesn’t care if it’s open source or not, it might as well be. There was no conflict of interest here and it&amp;rsquo;s not related to my day job, so I was free to own the rights for this specific app.&lt;/p&gt;&lt;h2 id=&#34;why-im-not-afraid-to-share-this&#34;&gt;Why I’m not afraid to share this?&lt;/h2&gt;&lt;p&gt;What if someone stills my idea or copies my code?&lt;/p&gt;&lt;p&gt;Well, good luck with that. The app doesn’t do any income, so why would anyone?&lt;/p&gt;&lt;p&gt;My code at times is not that polished, what if people judge me?&lt;/p&gt;&lt;p&gt;I have pretty high standards when it comes to development and sometimes, the code I submit on personal projects is not my best work (but it’s still pretty solid overall, I think). Well, I don’t get paid for writing this and I have limited time to work on the project on the weekends, so I will be cutting some corners. As long as I still have fun writing this, things are good.&lt;/p&gt;&lt;p&gt;On the other hand, it’s cool to share your work like this. It proves that you can build complex things that work and not just tweak things that other have built.&lt;/p&gt;&lt;h2 id=&#34;show-casing-real-applications&#34;&gt;Show casing real applications&lt;/h2&gt;&lt;p&gt;There aren’t many examples of complete apps that are publicly accessible like this. It’s a good showcase of solving real problems.&lt;/p&gt;&lt;p&gt;Source code of complete working applications was a great resource to me when I worked in a software house a couple years back. I could easily see how a specific problem was solved and how things were architected. It saved a lot of time.&lt;/p&gt;&lt;p&gt;To give you some taste of what I have built:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Portfolio overview page&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/account_overview.png&#34; alt=&#34;account overview&#34;&gt;&lt;/p&gt;&lt;ol start=&#34;2&#34;&gt;&lt;li&gt;Onboarding&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/transactions.png&#34; alt=&#34;onboarding transaction&#34;&gt;&lt;/p&gt;&lt;ol start=&#34;3&#34;&gt;&lt;li&gt;Realized gains&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/gains.png&#34; alt=&#34;gains&#34;&gt;&lt;/p&gt;&lt;p&gt;And much more&amp;hellip; Play with it if you want at &lt;a href=&#34;https://invertimo.com&#34;&gt;invertimo.com&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&#34;technology-used&#34;&gt;Technology used&lt;/h2&gt;&lt;p&gt;I use pretty standard (&lt;a href=&#34;https://tinystruggles.com/posts/boring_technologies/&#34;&gt;boring&lt;/a&gt;) technology for invertimo. Here are the details:&lt;/p&gt;&lt;p&gt;Backend:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;python 3.8&lt;/li&gt;&lt;li&gt;django&lt;/li&gt;&lt;li&gt;PostgreSQL&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Frontend:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;JavaScript &amp;amp; CSS (I haven&amp;rsquo;t jumped on the TypeScript bandwagon yet!)&lt;/li&gt;&lt;li&gt;React&lt;/li&gt;&lt;li&gt;webpack - package management&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://mui.com/&#34;&gt;material ui&lt;/a&gt; - complex frontend components&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Infrastructure:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;docker - all app components run in containers and are be built in a reproducible way&lt;/li&gt;&lt;li&gt;nginx - web server reverse proxy&lt;/li&gt;&lt;li&gt;Digital Ocean - simple and friendly cloud provider with very clear billing&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This set of technologies serves me pretty well for building complex web apps.I believe with some tweaks it could also scale very well.&lt;/p&gt;&lt;h2 id=&#34;major-inspirations-for-setup--architecture&#34;&gt;Major inspirations for setup &amp;amp; architecture&lt;/h2&gt;&lt;p&gt;It is hard to create things entirely from scratch and I didn&amp;rsquo;t do that. I found the resources below especially helpful:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&#34;https://www.obeythetestinggoat.com/&#34;&gt;https://www.obeythetestinggoat.com/&lt;/a&gt; - a fantastic open resource about how to build and test a django application end to end&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://www.saaspegasus.com/guides/modern-javascript-for-django-developers/&#34;&gt;https://www.saaspegasus.com/guides/modern-javascript-for-django-developers/&lt;/a&gt; - this was especially helpful for putting Django and JS frontend together.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you build modern django apps, those resources are golden.&lt;/p&gt;&lt;h2 id=&#34;what-next&#34;&gt;What next&lt;/h2&gt;&lt;p&gt;I don&amp;rsquo;t have much commercial plans with this app, but building it is quite fun and I&amp;rsquo;m solving my own problems. There is still a bunch of features I could use that I haven&amp;rsquo;t built yet.&lt;/p&gt;&lt;p&gt;In Q1 2022 I plan to build support for crypto related tracking (e.g. income from staking or crypto savings) and &lt;a href=&#34;https://www.binance.com/en&#34;&gt;binance&lt;/a&gt; (popular centralized exchange) integration. It is going to be a lot of work, I&amp;rsquo;m sure.&lt;/p&gt;&lt;p&gt;If you enjoy this post and what I am doing, please follow me on &lt;a href=&#34;https://twitter.com/attilczuk&#34;&gt;twitter&lt;/a&gt;. Also feel free to say hi!&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>I prefer boring technologies</title>
       <link>https://tinystruggles.com/posts/boring_technologies/</link>
       <pubDate>Thu, 30 Dec 2021 10:45:59 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/boring_technologies/</guid>
       <description>&lt;p&gt;When I start new projects I try to build them with boring technologies that I used before and I try to limit picking up new libraries, tools and technologies to a minimum.&lt;/p&gt;&lt;p&gt;This is because I want to build actual products and not to do projects for the sake of learning exciting technology.&lt;/p&gt;&lt;p&gt;I chose boring technologies because:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;they are proven &amp;amp; dependable&lt;/li&gt;&lt;li&gt;I can focus on building a product&lt;/li&gt;&lt;li&gt;challenges specific to a product keep it interesting and I don’t need shiny tech novelty&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you want to build cool things, you should consider doing the same.&lt;/p&gt;&lt;h2 id=&#34;boring--proven--dependable&#34;&gt;Boring == proven &amp;amp; dependable&lt;/h2&gt;&lt;p&gt;Often the reason why something is boring is that it’s been around for a while and it’s popular.&lt;/p&gt;&lt;p&gt;Those are good things! This means that many bugs have already been found and fixed. This means that many people hit various issues with this technology and lived to tell their tale.&lt;/p&gt;&lt;p&gt;On the other hand many new shiny things have lots of unknown issues. You might be the first person hitting a particular problem and that can happen to you every month.&lt;/p&gt;&lt;p&gt;Another issue with shiny tech is that it might not be here anymore after a couple years if they don’t get wide adoption. Boring technologies on the other hand have communities of maintainers and many companies behind them.&lt;/p&gt;&lt;h2 id=&#34;boring--familiar--can-focus-on-building-a-product&#34;&gt;Boring == familiar → can focus on building a product&lt;/h2&gt;&lt;p&gt;What is boring is often familiar which means that I am more fluent using it.&lt;/p&gt;&lt;p&gt;Instead of focusing on how I can implement an API, I can focus on what I want within the API. The implementation is straightforward.&lt;/p&gt;&lt;p&gt;I really believe that you have a limited number of “innovation tokens” while working on something new. You can spend them innovating on the product or on learning a novel technology.&lt;/p&gt;&lt;p&gt;However unless the technology itself is a distinguishing factor for your business or product, you should minimize using your tokens on the novel tech. Your users won’t care if you are using Vue or Svelte or React as long as your app is fast and easy to use and solves their problem.&lt;/p&gt;&lt;h3 id=&#34;technology-stack-vs-technology-alternatives&#34;&gt;Technology stack vs technology alternatives&lt;/h3&gt;&lt;p&gt;Is the framework the fastest and the coolest? Probably not, it’s possible that there are alternatives that are slightly better.&lt;/p&gt;&lt;p&gt;I want to focus on learning and mastering a set of complementary and not alternative technologies, because this makes me more effective developing things full stack myself.Alternative technologies would be like React and Angular or Python and Ruby. Instead, I develop an expertise in a stack of complementary technologies, like JavaScript and SQL or python and dev ops.&lt;/p&gt;&lt;p&gt;Having many alternative solutions deployed increases your complexity significantly.  The technology combinations in your stack grow very fast.So keep your tech life simple and focus on the product instead.&lt;/p&gt;&lt;h3 id=&#34;boring-technology---but-an-interesting-project&#34;&gt;Boring technology - but an interesting project?&lt;/h3&gt;&lt;p&gt;Won’t you get bored if you are only using proven technologies or keep using the same libraries over and over?&lt;/p&gt;&lt;p&gt;This is a risk to some extent, especially if you have lots of boilerplate. Many products require the same things: users, authentication, payments, email. But after you get the basics out of the way, then what is left is specific to the problem you are going to solve and that is different with each product.&lt;/p&gt;&lt;p&gt;And there are so many unique challenges to each product, the data modeling, API design, UX, design and so on!&lt;/p&gt;&lt;p&gt;Building end to end products is exciting!&lt;/p&gt;&lt;h3 id=&#34;summary&#34;&gt;Summary&lt;/h3&gt;&lt;p&gt;To sum up:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;boring technologies are more dependable&lt;/li&gt;&lt;li&gt;focusing on couple of solutions creating a consistent stack helps me master them and makes my life simpler&lt;/li&gt;&lt;li&gt;developing products is often more interesting than dabbling in technology for technology&amp;rsquo;s sake&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you want to read more about boring technologies, I found these &lt;a href=&#34;http://boringtechnology.club/&#34;&gt;slides&lt;/a&gt; pretty helpful.&lt;/p&gt;&lt;p&gt;If like stuff that I write, please follow me on &lt;a href=&#34;https://twitter.com/intent/follow?original_referer=https%3A%2F%2Ftinystruggles.com%2F&amp;amp;ref_src=twsrc%5Etfw%7Ctwcamp%5Ebuttonembed%7Ctwterm%5Efollow%7Ctwgr%5Eattilczuk&amp;amp;region=follow_link&amp;amp;screen_name=attilczuk&#34;&gt;twitter&lt;/a&gt; where I post about technology and my Indie Hacker journey.&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Learning can be a waste of time</title>
       <link>https://tinystruggles.com/posts/learning_waste_of_time/</link>
       <pubDate>Sat, 13 Nov 2021 10:45:59 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/learning_waste_of_time/</guid>
       <description>&lt;p&gt;Learning and studying isn&amp;rsquo;t universally a good thing.&lt;/p&gt;&lt;p&gt;I found that saying &amp;ldquo;sometimes spending some time studying and learning can be a waste of time&amp;rdquo; is very contentious. Especially in a quickly evolving tech environment.&lt;/p&gt;&lt;p&gt;If you stop learning and become rigid in your ways, you become obsolete. If you stop learning you miss the opportunity to get more efficient and be able to do more in a shorter time.&lt;/p&gt;&lt;p&gt;But, we shouldn&amp;rsquo;t just glorify the activity, because it&amp;rsquo;s essential when executed well. Not all studying or even learning is a good use of your time.&lt;/p&gt;&lt;h2 id=&#34;similarities-with-finance&#34;&gt;Similarities with finance&lt;/h2&gt;&lt;p&gt;Let&amp;rsquo;s compare learning to financial investing. Investing is generally perceived as good for you. But is spending money buying &amp;ldquo;investments&amp;rdquo; always a good thing? What if your &amp;ldquo;investment&amp;rdquo; has zero return of investment and is just a waste of resources? All because you didn&amp;rsquo;t do enough research and made poor capital allocation?&lt;/p&gt;&lt;p&gt;Or when you do too many investments and get overstretched. Then you can&amp;rsquo;t put enough attention to the project and then it fails.&lt;/p&gt;&lt;p&gt;Or if you got excited by something shiny (hello, crypto-dog-coins) and lost lots of money because of speculation? Maybe in that case it would be better not to &amp;ldquo;invest&amp;rdquo; at all?&lt;/p&gt;&lt;p&gt;Your time is probably your most precious asset. When you spend time on learning, you can&amp;rsquo;t take back this time. Let&amp;rsquo;s make it count. Let&amp;rsquo;s start with two major problems I see.&lt;/p&gt;&lt;h2 id=&#34;learning-that-doesnt-advance-you-toward-your-goals&#34;&gt;Learning that doesn&amp;rsquo;t advance you toward your goals&lt;/h2&gt;&lt;p&gt;If your goals are ambitious you will have to learn and evolve to meet them. No doubt.&lt;/p&gt;&lt;p&gt;But even with the best intentions you can get it wrong.&lt;/p&gt;&lt;h3 id=&#34;learning-wrong-things-for-your-goals&#34;&gt;Learning wrong things for your goals&lt;/h3&gt;&lt;p&gt;You might be asking, who would fall into this trap? It&amp;rsquo;s surprisingly easy if you don&amp;rsquo;t doenough pre-learning research or lack self awareness. What skills and knowledge will unlock your progress?&lt;/p&gt;&lt;p&gt;At times, it&amp;rsquo;s better to go broad than deep.Maybe you should take a UX or Project Management course instead of learning another JS framework?At other times, it&amp;rsquo;s better to go deep.&lt;/p&gt;&lt;p&gt;And maybe what you should do is invest in the soft skills rather than the hard skills?&lt;/p&gt;&lt;p&gt;Let&amp;rsquo;s take Joe as an example, Joe wants to get promoted in his software Web Development job. Joe is ambitious and willhappily spend a lot of time learning. Joe decides that they will read a book about software architecture,so they will be more ready to be a senior engineer.&lt;/p&gt;&lt;p&gt;What you don&amp;rsquo;t know about Joe, is that Joe is held back by a different problem. He regularly underestimates how long his tasks will take,dismisses the need for project meetings, works in long stretches without updating anybody and fails to deliver his commitments on time.&lt;/p&gt;&lt;p&gt;Joe doesn&amp;rsquo;t have the trust of his teammates and won&amp;rsquo;t get promoted regardless of how good his JS, Python, Rust&amp;hellip; software architecture, distributed systems etc skills are.He needs to understand the basics of project management and become a reliable team mate.&lt;/p&gt;&lt;h3 id=&#34;studying-as-a-form-of-procrastination&#34;&gt;Studying as a form of procrastination&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;&amp;ldquo;I just need to learn a bit more before I &amp;hellip;&amp;rdquo;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Would you do heart surgery without training? I hope not. But for many things just a basic knowledge is enough and youwill learn much more from doing. Or you might even discover that you were learning wrong things.&lt;/p&gt;&lt;p&gt;Warning signs that your learning is a form of procrastination:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;there isn&amp;rsquo;t a definite end or it keeps moving&lt;/li&gt;&lt;li&gt;you have been reading/listening to to the same type of material for a very long time (stuck in beginner mode)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The problem here might be either a lack of strategy or confidence. If it&amp;rsquo;s the strategy, put some time into thinking on a more meta level.&lt;/p&gt;&lt;p&gt;If it&amp;rsquo;s fear or lack of confidence, think about the actual risks you are facing:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;What is at stake? E.g. bankruptcy, your life, health or maybe just a bit of an embarrassment?&lt;/li&gt;&lt;li&gt;Can you go back and learn more if needed without sabotaging the goal?&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Maybe the risk of failure is less bad than the cost of inaction and time put into &amp;ldquo;learning&amp;rdquo;?Maybe it&amp;rsquo;s just not for you and you should move on completely.&lt;/p&gt;&lt;h2 id=&#34;when-you-think-you-are-learning-but-not-really&#34;&gt;When you think you are learning, but not really&lt;/h2&gt;&lt;p&gt;Another reason why learning can be a big waste of time is when you are learning the right things,but it either doesn&amp;rsquo;t absorb or doesn&amp;rsquo;t stick.&lt;/p&gt;&lt;h3 id=&#34;absorbing-knowledge-poor-learning-habits&#34;&gt;Absorbing Knowledge: Poor learning habits&lt;/h3&gt;&lt;p&gt;There are lots of ways to absorb new knowledge and skill, many of which are ineffective.&lt;/p&gt;&lt;p&gt;Some questions for you:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Are you absorbing the knowledge passively or actively?&lt;/li&gt;&lt;li&gt;How do you test your new understanding?&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you are passive and don&amp;rsquo;t test your understanding then you are likely not learning very well.You might feel like you are and you might think like doing it in a different way, would be slowerand less efficient.&lt;/p&gt;&lt;p&gt;Learning is changing, if nothing changes, there is no learning. Your brain has to go through the reps, you need to develop new a way of thinking.This is why learning with a project is good, or when there is a quiz or an assignment where you have to practice your knowledge.&lt;/p&gt;&lt;p&gt;Before engaging into studying you can look out for learning materials that are more active and if what you have is just reading material, you can stillmake it more active yourself.&lt;/p&gt;&lt;h3 id=&#34;retaining-knowledge-lack-of-organization-and-process&#34;&gt;Retaining Knowledge: lack of organization and process&lt;/h3&gt;&lt;p&gt;I took plenty of notes in college. Do you know what I did with those notebooks after the courses finished?I threw them away to trash as I knew that I would probably never open them again.&lt;/p&gt;&lt;p&gt;As you continue learning and build on top of what you are learning, that&amp;rsquo;s probably fine, because the old material is stillthere. Similarly if you apply your skills soon after, you can maintain them and build upon them.&lt;/p&gt;&lt;p&gt;I did take a bunch of courses in quantum physics. They were actually pretty good, there were many assignments,math problems to solve, simulations to program, practical experiments to do.It was great, I learned a lot, I got good grades, but I don&amp;rsquo;t remember nor  use almost any of it now. I work as an engineer in Software, not as a physicist.&lt;/p&gt;&lt;p&gt;There are many things like this where I put a lot of effort and years later it&amp;rsquo;s mostly forgotten. Could I relearn it - probably. But it&amp;rsquo;s still painful to me.&lt;/p&gt;&lt;p&gt;It&amp;rsquo;s painful enough to me that I started to be more strategic about my knowledge retention.&lt;/p&gt;&lt;h4 id=&#34;living-note-taking-system&#34;&gt;Living note taking system&lt;/h4&gt;&lt;p&gt;I generally write things down for myself for two reasons:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;to process stuff out - figure out a plan, explore a problem, etc&lt;/li&gt;&lt;li&gt;to make a resource I can use later&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Notebooks or Google Doc documents work perfectly fine for the first goal. But in my case don&amp;rsquo;t workvery well for the second goal. It&amp;rsquo;s easy to forget about a given notebook, it&amp;rsquo;s not that easy tosearch and I just find it that I never refer back to them. In the past I tried evernote and google keep, but it would quickly turn into a mess.&lt;/p&gt;&lt;p&gt;And then I discovered &lt;a href=&#34;http://notion.com/&#34;&gt;Notion&lt;/a&gt;. It is visually pleasing, allows easy structuring, reorganizing and adding information (and has a lot of features!).It&amp;rsquo;s my tool for organizing information for the long term. There is a ton of materials out there about it and the tool is free for individuals.Go try it!&lt;/p&gt;&lt;h4 id=&#34;process-around-recall-reinforcing-skills&#34;&gt;Process around recall (reinforcing skills)&lt;/h4&gt;&lt;p&gt;Another trap with learning is not thinking about maintaining the skill. If you learn a skill to then use it immediately, you might be fine.But more often than not, you can find yourself forgetting the things you should have supposedly learned and getting frustrated.&lt;/p&gt;&lt;p&gt;Compound interest is a great concept, if you keep improving by 1% every day, you will end up ~37 times better over a year. Not 365% better, 3778% better!This is incredible! But what if you have a skill or knowledge attrition at the same time? +1% is offset by -1% (or more!) and you end up on average improving 0% per dayand 0 percent over a year and you get stuck. You might be thinking, why am I not leveling up, I keep learning!?&lt;/p&gt;&lt;p&gt;The good news is that retaining knowledge requires less effort than acquiring knowledge in the first place. But it&amp;rsquo;s still something that you need to beaware of, especially if the new things seem more exciting and satisfying to learn.&lt;/p&gt;&lt;p&gt;To be consistent, I would recommend having a process that you follow. I use &lt;a href=&#34;https://ankiweb.net/&#34;&gt;Anki&lt;/a&gt; for &lt;a href=&#34;https://en.wikipedia.org/wiki/Spaced_repetition&#34;&gt;Spaced Repetition&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;It&amp;rsquo;s an app in which you can create cards and then practice recalling their contents. There is some art in making the cards, but you can always change them if they are not that great.If you can recall the note (or answer your question) well, then you will have a review scheduled after a longer period. If you forgot, you will review it again tomorrow. The better you remember the given card,the farther in the future it gets scheduled. I spend about 20 minutes a day every day reviewing the cards. It&amp;rsquo;s mostly foreign language vocab.&lt;/p&gt;&lt;h2 id=&#34;keep-on-learning-but-learn-better&#34;&gt;Keep on learning, but learn better&lt;/h2&gt;&lt;p&gt;I hope you learned something reading this! Overall, I&amp;rsquo;m not an enemy of learning, I am obsessed with learningand I am always working on my skills, reading books, articles and taking courses.&lt;/p&gt;&lt;p&gt;But it can be a waste of time, I wasted a lot of time learning myself. I could have had more time, I could have been less stressed and busy.I could have learned more and more useful things.&lt;/p&gt;&lt;p&gt;The key is to not do things in an unexamined way. Be self aware, be strategic, learn about learning, improve over time and keep on learning.&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Django Rest Framework Recipes</title>
       <link>https://tinystruggles.com/posts/drf_recipes/</link>
       <pubDate>Mon, 26 Jul 2021 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/drf_recipes/</guid>
       <description>&lt;div class=&#34;toc&#34;&gt;    &lt;h3&gt;Table of contents&lt;/h3&gt;    &lt;nav id=&#34;TableOfContents&#34;&gt;  &lt;ul&gt;    &lt;li&gt;&lt;a href=&#34;#introduction&#34;&gt;Introduction&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#piece-of-api-used-for-examples&#34;&gt;Piece of API used for examples&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#recipes-structure&#34;&gt;Recipes structure&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#only-show-entities-related-to-the-current-user&#34;&gt;Only show entities related to the current user&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#different-serializers-for-different-methods-within-a-viewset&#34;&gt;Different serializers for different methods within a viewset&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#add-fields-on-the-fly-that-arent-present-on-the-model&#34;&gt;Add fields on the fly that aren&amp;rsquo;t present on the model&lt;/a&gt;      &lt;ul&gt;        &lt;li&gt;&lt;a href=&#34;#based-on-queryset-annotation&#34;&gt;Based on queryset annotation&lt;/a&gt;&lt;/li&gt;        &lt;li&gt;&lt;a href=&#34;#based-on-a-dynamic-function&#34;&gt;Based on a dynamic function&lt;/a&gt;&lt;/li&gt;      &lt;/ul&gt;    &lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#pass-additional-data-to-the-serializer&#34;&gt;Pass additional data to the serializer&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#use-a-serializer-for-the-query-parameters&#34;&gt;Use a serializer for the query parameters&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#use-string-value-in-an-api-for-a-field-that-has-more-efficient-db-representation-enum&#34;&gt;Use string value in an API for a field that has more efficient DB representation (enum)&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#unique-together-with-a-user-that-is-not-set-in-the-form&#34;&gt;Unique together with a user that is not set in the form&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#viewset-using-all-these-patterns&#34;&gt;ViewSet using all these patterns&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;&lt;a href=&#34;#recommendations&#34;&gt;Recommendations&lt;/a&gt;&lt;/li&gt;  &lt;/ul&gt;&lt;/nav&gt;&lt;/div&gt;&lt;h2 id=&#34;introduction&#34;&gt;Introduction&lt;/h2&gt;&lt;p&gt;One of my favorite tools in my app development toolkit is &lt;a href=&#34;https://www.django-rest-framework.org/&#34;&gt;django rest framework&lt;/a&gt; (drf), that makes developing REST APIs with python and django easy and fun.It&amp;rsquo;s easy to set up, extensible and saves so much time.&lt;/p&gt;&lt;p&gt;The documentation of django rest framework is pretty extensive, there is a great &lt;a href=&#34;https://www.django-rest-framework.org/tutorial/quickstart/&#34;&gt;tutorial&lt;/a&gt;, api docs and plenty of examples.But reality is usually a bit more complex and you need to customize your use of the framework.&lt;/p&gt;&lt;p&gt;In this article I&amp;rsquo;m going to share a bunch ofrecipes from my use of django rest framework taken from my latest project. They all come from just a single &lt;a href=&#34;https://www.django-rest-framework.org/api-guide/viewsets/&#34;&gt;ViewSet&lt;/a&gt; (set of related api endpoints)!&lt;/p&gt;&lt;p&gt;At the end of the article I will show how all those bits fit together.&lt;/p&gt;&lt;p&gt;This article doesn&amp;rsquo;t provide any introduction to &lt;a href=&#34;https://docs.djangoproject.com/en/3.2/intro/tutorial01/&#34;&gt;django&lt;/a&gt; or the &lt;a href=&#34;https://www.django-rest-framework.org/&#34;&gt;django rest framework&lt;/a&gt;. Please read the official docs.&lt;/p&gt;&lt;h2 id=&#34;piece-of-api-used-for-examples&#34;&gt;Piece of API used for examples&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve been working on a feature in the &lt;a href=&#34;https://github.com/ilonajulczuk/invertimo&#34;&gt;invertimo app&lt;/a&gt; where users can add multiple different investment accounts.The api is consumed by a react frontend and it&amp;rsquo;s customized by the need of the frontend app.&lt;/p&gt;&lt;p&gt;I&amp;rsquo;m using drf &lt;code&gt;ViewSet&lt;/code&gt; to provide the following:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;list&lt;/code&gt; endpoint &amp;lsquo;/accounts/&amp;rsquo; that lists the accounts based on the model and provides a bunch of additional fields within each model&lt;/li&gt;&lt;li&gt;very rich detail &lt;code&gt;get&lt;/code&gt; endpoint &amp;lsquo;/accounts/id/&amp;rsquo; for retrieving detailed account data that takes additional parameters (from_date, to_date)&lt;/li&gt;&lt;li&gt;&lt;code&gt;create&lt;/code&gt; and &lt;code&gt;update&lt;/code&gt; endpoints that only allow to set or touch limited number of parameters&lt;/li&gt;&lt;li&gt;delete endpoint that in this case is very standard&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Examples are using python type hints and are coming from a project using python 3.8 (at the time of writing). If you want to know more about setting up typechecking for django rest framework I recommend this &lt;a href=&#34;https://sobolevn.me/2019/08/typechecking-django-and-drf&#34;&gt;article&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&#34;recipes-structure&#34;&gt;Recipes structure&lt;/h2&gt;&lt;p&gt;The recipes are sorted from most common to more complex and are grouped together whenthey relate to a similar class of problems. I will provide a bit of a context, general motivation for using a given recipe and an example.&lt;/p&gt;&lt;h2 id=&#34;only-show-entities-related-to-the-current-user&#34;&gt;Only show entities related to the current user&lt;/h2&gt;&lt;p&gt;My app supports multiple users and they each have their own private data. I don&amp;rsquo;t want one userto see other user&amp;rsquo;s private data.&lt;/p&gt;&lt;p&gt;Motivation:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;private data visible only to the owning user&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Implementation:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;filter the queryset&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The best place to filter the queryset is to override &lt;code&gt;get_queryset&lt;/code&gt; method provided by the parent class.&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AccountsViewSet&lt;/span&gt;(viewsets&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ModelViewSet):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    permission_classes &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [permissions&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IsAuthenticated]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    serializer_class &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; AccountSerializer&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    basename &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;account&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get_queryset&lt;/span&gt;(self) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; QuerySet[models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Account]:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;assert&lt;/span&gt; isinstance(self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;request&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;user, User)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        queryset &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Account&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;objects&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(user&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;request&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;user)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; queryset&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can get the user from the &lt;code&gt;self.request.user&lt;/code&gt; .&lt;/p&gt;&lt;p&gt;&lt;code&gt;assert isinstance(self.request.user, User)&lt;/code&gt; is totally optional and done for the sake of narrowing down the type.In this case I can assert that the user is indeed a User because I enforce that the user has to the authenticated with:&lt;code&gt;permission_classes = [permissions. IsAuthenticated]&lt;/code&gt; .&lt;/p&gt;&lt;h2 id=&#34;different-serializers-for-different-methods-within-a-viewset&#34;&gt;Different serializers for different methods within a viewset&lt;/h2&gt;&lt;p&gt;&lt;a href=&#34;https://www.django-rest-framework.org/api-guide/serializers/&#34;&gt;Serializers&lt;/a&gt; in drf allow easy serialization (e.g to json) and deserialization (to native python) in your API.&lt;/p&gt;&lt;p&gt;By default there is one serializer class for a single ViewSet, even if it contains multiple separate views.&lt;/p&gt;&lt;p&gt;This is often fine, but at times you want to do it differently. See a simple example below:&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/simple_serializers.png&#34; alt=&#34;simple serializers&#34;&gt;&lt;/p&gt;&lt;p&gt;Notice that there are less fields in the create form than within serialized values.&lt;/p&gt;&lt;p&gt;Motivation:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;rich display of related fields for read only version&lt;/li&gt;&lt;li&gt;fields that are computed based on more complex logic&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Ways to do it:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;reimplement each method you want to override the serializer for (repetitive)&lt;/li&gt;&lt;li&gt;override &lt;code&gt;get_serializer_class&lt;/code&gt; (recommended!)&lt;/li&gt;&lt;/ul&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AccountsViewSet&lt;/span&gt;(viewsets&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ModelViewSet):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    permission_classes &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [permissions&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IsAuthenticated]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    serializer_class &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; AccountSerializer&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    basename &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;account&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get_serializer_class&lt;/span&gt;(self):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;action &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; (&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;create&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;update&amp;#34;&lt;/span&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; AccountEditSerializer&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;action &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;retrieve&amp;#34;&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; AccountWithValuesSerializer&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; AccountSerializer&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the only difference between serializers you have is that some fields are read only and shouldn&amp;rsquo;t be used in views that are updating the data,you might want to mark those fields are &lt;a href=&#34;https://www.django-rest-framework.org/api-guide/serializers/#specifying-read-only-fields&#34;&gt;read only&lt;/a&gt; instead of changing the serializer.&lt;/p&gt;&lt;h2 id=&#34;add-fields-on-the-fly-that-arent-present-on-the-model&#34;&gt;Add fields on the fly that aren&amp;rsquo;t present on the model&lt;/h2&gt;&lt;p&gt;Another common case I encountered while developing APIs was adding more data to the serialized model instances.&lt;/p&gt;&lt;p&gt;I present two recipes here:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;using queryset annotation&lt;/li&gt;&lt;li&gt;using a method on a serializer&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;based-on-queryset-annotation&#34;&gt;Based on queryset annotation&lt;/h3&gt;&lt;p&gt;Motivation:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;add a field based on the result SQL query, e.g. count of related entities&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;How to do it:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;annotate the queryset&lt;/li&gt;&lt;li&gt;update the serializer to display new fields&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Here I&amp;rsquo;m adding &lt;code&gt;positions_count&lt;/code&gt; and &lt;code&gt;transactions_count&lt;/code&gt; :&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AccountsViewSet&lt;/span&gt;(viewsets&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ModelViewSet):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    permission_classes &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [permissions&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IsAuthenticated]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    serializer_class &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; AccountSerializer&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    pagination_class &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; LimitOffsetPagination&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    basename &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;account&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get_queryset&lt;/span&gt;(self) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; QuerySet[models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Account]:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;assert&lt;/span&gt; isinstance(self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;request&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;user, User)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        queryset &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Account&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;objects&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(user&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;request&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;user)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;annotate(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            positions_count&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;Count(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;positions&amp;#34;&lt;/span&gt;, distinct&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            transactions_count&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;Count(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;positions__transactions&amp;#34;&lt;/span&gt;, distinct&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; queryset&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Define additional fields within the serializer by specifying the fields and adding them to the list in the meta.New fields here are &lt;code&gt;positions_count&lt;/code&gt; and &lt;code&gt;transactions_count&lt;/code&gt; .&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AccountSerializer&lt;/span&gt;(serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ModelSerializer[Account]):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    positions_count &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IntegerField()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    transactions_count &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IntegerField()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Meta&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        model &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Account&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        fields &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;nickname&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;description&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;balance&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;last_modified&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;positions_count&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;transactions_count&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The great thing about adding fields with queryset annotations is that it&amp;rsquo;s also efficient and prevents using an excessive number of SQL queries.&lt;/p&gt;&lt;p&gt;Useful links:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&#34;https://www.django-rest-framework.org/api-guide/serializers/#specifying-which-fields-to-include&#34;&gt;serializer fields to be included&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&#34;based-on-a-dynamic-function&#34;&gt;Based on a dynamic function&lt;/h3&gt;&lt;p&gt;Sometimes you can&amp;rsquo;t express the additional field you need this way. For example you need to call a method on the instance of the object to get the value you need.&lt;/p&gt;&lt;p&gt;There is an easy way with drf to do this with &lt;code&gt;SerializerMethodField&lt;/code&gt; .&lt;/p&gt;&lt;p&gt;Motivation:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;add a field to serialized data that requires custom logic&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;How to do it:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;define a field as &lt;code&gt;serializers.SerializerMethodField()&lt;/code&gt;&lt;/li&gt;&lt;li&gt;add it to &lt;code&gt;fields&lt;/code&gt; list in &lt;code&gt;Meta&lt;/code&gt;&lt;/li&gt;&lt;li&gt;define &lt;code&gt;get_myfieldname&lt;/code&gt; method&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Here is an example where I define new field called &lt;code&gt;values&lt;/code&gt; :&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AccountWithValuesSerializer&lt;/span&gt;(serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ModelSerializer[Account]):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    positions_count &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IntegerField()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    transactions_count &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IntegerField()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    currency &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; CurrencyField()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    values &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;SerializerMethodField()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Meta&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        model &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Account&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        fields &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;currency&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;nickname&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;description&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;balance&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;last_modified&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;positions_count&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;transactions_count&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;values&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get_values&lt;/span&gt;(self, obj):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        from_date &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;from_date&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        to_date &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;to_date&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; obj&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;value_history_per_position(from_date, to_date)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this example I use some additional data from &lt;code&gt;self.context&lt;/code&gt; that brings me to my next recipe.&lt;/p&gt;&lt;h2 id=&#34;pass-additional-data-to-the-serializer&#34;&gt;Pass additional data to the serializer&lt;/h2&gt;&lt;p&gt;Motivation:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;use additional data to generate and additional field&lt;/li&gt;&lt;li&gt;perform additional validation&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;How to do it:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;override &lt;code&gt;get_serializer_context&lt;/code&gt;&lt;/li&gt;&lt;li&gt;the data can come e.g. from self.request, e.g. &lt;code&gt;self.request.user&lt;/code&gt; of &lt;code&gt;self.request.query_params&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Example from the &lt;code&gt;ViewSet&lt;/code&gt; code:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get_serializer_context&lt;/span&gt;(self) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;  Dict[str, Any]:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        context: Dict[str, Any] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; super()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_serializer_context()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        query &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; FromToDatesSerializer(data&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;request&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;query_params)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;request&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;request&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; context&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and then the set value can be used inside the serializer:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;request &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;context&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;request&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;use-a-serializer-for-the-query-parameters&#34;&gt;Use a serializer for the query parameters&lt;/h2&gt;&lt;p&gt;Serializers transform data between formats such as json and native python, they also provide a good place toput your validation logic. Well, you can use a serializer to extract and validate data from the query parameters (also known as URL params).&lt;/p&gt;&lt;p&gt;Motivation:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;use additional query parameters within a ViewSet and have them validated&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;How to do it:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;define a custom Serializer for the data you expect in your query params (see that this serializer is not based on the ModelSerializer)&lt;/li&gt;&lt;li&gt;use it within your view:&lt;ul&gt;&lt;li&gt;initialize with &lt;code&gt;MySerializer(data=self.request.query_params)&lt;/code&gt;&lt;/li&gt;&lt;li&gt;validate and extract the data&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;You can combine with a previous technique of passing additional data through the serializer context like follows:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;FromToDatesSerializer&lt;/span&gt;(serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Serializer[Any]):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    from_date &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;DateField(required&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;False&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    to_date &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;DateField(required&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;False&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Useful links:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&#34;https://www.django-rest-framework.org/api-guide/serializers/#serializers&#34;&gt;serializers documentation&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AccountsViewSet&lt;/span&gt;(viewsets&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ModelViewSet):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get_serializer_context&lt;/span&gt;(self) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;  Dict[str, Any]:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        context: Dict[str, Any] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; super()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_serializer_context()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        query &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; FromToDatesSerializer(data&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;request&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;query_params)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;request&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;request&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; query&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;is_valid(raise_exception&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; query&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;validated_data&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;query_data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; data&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;from_date&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;query_data&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;from_date&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                datetime&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;date&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;today() &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; datetime&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;timedelta(days&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt;),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            )&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;to_date&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;query_data&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;to_date&amp;#34;&lt;/span&gt;, datetime&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;date&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;today())&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; context&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;use-string-value-in-an-api-for-a-field-that-has-more-efficient-db-representation-enum&#34;&gt;Use string value in an API for a field that has more efficient DB representation (enum)&lt;/h2&gt;&lt;p&gt;If a field can only have limited number of options, enums are a great choice.Django provides &lt;code&gt;TextChoices&lt;/code&gt; , &lt;code&gt;IntegerChoices&lt;/code&gt; , and &lt;code&gt;Choices&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://docs.djangoproject.com/en/3.0/ref/models/fields/#enumeration-types&#34;&gt;enumeration types&lt;/a&gt; to make it very easy.&lt;/p&gt;&lt;p&gt;My preferred field is the &lt;code&gt;IntegerChoices&lt;/code&gt; because it will end up using much less space in the database even if the represented value is a string.&lt;/p&gt;&lt;p&gt;I have a currency field defined as follows:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Currency&lt;/span&gt;(models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IntegerChoices):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    EUR &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, _(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;EUR&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    GBP &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;, _(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;GBP&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    USD &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;, _(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;USD&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    GBX &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;4&lt;/span&gt;, _(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;GBX&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;currency_enum_from_string&lt;/span&gt;(currency: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; Currency:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; Currency[currency]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;KeyError&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ValueError&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Unsupported currency &amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; currency)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;currency_string_from_enum&lt;/span&gt;(currency: Currency) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; str:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; Currency(currency)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;label&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Account&lt;/span&gt;(models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Model):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    user &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ForeignKey(User, on_delete&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CASCADE)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    currency &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IntegerField(choices&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;Currency&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;choices, default&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;Currency&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;EUR)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    nickname &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CharField(max_length&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;200&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    description &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;TextField(blank&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There are only 4 different values and they are stored very efficiently in the database.&lt;/p&gt;&lt;p&gt;However, I don&amp;rsquo;t want my API to expect value &amp;lsquo;1&amp;rsquo; for EUR. I would much rather have &amp;ldquo;EUR&amp;rdquo; to represent &amp;ldquo;EUR&amp;rdquo; and not expose that I use integers to represent it in the database.&lt;/p&gt;&lt;p&gt;Motivation:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Use different internal and external representation for a value, e.g. integer vs string&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;How to do it:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;create a new serializer Field class inheriting from a field that would be suitable for the internal representation&lt;/li&gt;&lt;li&gt;define &lt;code&gt;to_representation&lt;/code&gt; and &lt;code&gt;to_internal_value&lt;/code&gt; methods&lt;/li&gt;&lt;li&gt;specify that field in the serializer explicitly by using the newly defined class&lt;/li&gt;&lt;/ul&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;CurrencyField&lt;/span&gt;(serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IntegerField):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;to_representation&lt;/span&gt;(self, value):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;currency_string_from_enum(value)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;to_internal_value&lt;/span&gt;(self, value):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;currency_enum_from_string(value)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AccountEditSerializer&lt;/span&gt;(serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ModelSerializer[Account]):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# Currency needs to be changed from string to enum.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    currency &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; CurrencyField()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Useful links:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&#34;https://www.django-rest-framework.org/api-guide/fields/#custom-fields&#34;&gt;Custom fields in drf serializers&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&#34;https://docs.djangoproject.com/en/3.0/ref/models/fields/#enumeration-types&#34;&gt;django enumeration types&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;unique-together-with-a-user-that-is-not-set-in-the-form&#34;&gt;Unique together with a user that is not set in the form&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s a fairly common case to create objects for a user that is currently logged in.But what if you don&amp;rsquo;t pass the user in the form directly? And what if you want the objects e.g. name to be unique for a given user?&lt;/p&gt;&lt;p&gt;You can combine:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;passing additional value to the serializer with the serializer context by overriding &lt;code&gt;get_serializer_context&lt;/code&gt;&lt;/li&gt;&lt;li&gt;custom field validation (override the &lt;code&gt;validate_myfieldname&lt;/code&gt; method)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In this example the Account model has a constraint that nicknames have to be unique for a given user:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# In the model.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Meta&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        unique_together &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;nickname&amp;#34;&lt;/span&gt;]]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Within a serializer nickname field validation is overridden and if the uniqueness constraint is not satisfied,the serializer raises &lt;code&gt;serializers.ValidationError&lt;/code&gt;.&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Meta&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        model &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Account&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        fields &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;currency&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;nickname&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;description&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;validate_nickname&lt;/span&gt;(self, value):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# If user was also included in the serializer then unique_together&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# constraint would be automatically evaluated, but&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# since user is not included in the serializer the validation is&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# done manually.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        request &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;context&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;request&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; request &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; hasattr(request, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user&amp;#34;&lt;/span&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            user &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; request&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;user&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; Account&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;objects&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(user&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;user, nickname&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;value)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count() &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt; serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ValidationError(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;User already has an account with name: &amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;value&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                )&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; value&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Useful links:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&#34;https://www.django-rest-framework.org/api-guide/serializers/#validation&#34;&gt;serializer validation documentation&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&#34;viewset-using-all-these-patterns&#34;&gt;ViewSet using all these patterns&lt;/h2&gt;&lt;p&gt;Well, if you are curious what is the monstrosity I&amp;rsquo;ve been working on,I&amp;rsquo;m presenting you the code of it, showcasing how all these recipes fit together.&lt;/p&gt;&lt;p&gt;(Tests are not included, even though they exist! The entire codebase can be found &lt;a href=&#34;https://github.com/ilonajulczuk/invertimo&#34;&gt;here&lt;/a&gt;.)&lt;/p&gt;&lt;p&gt;&lt;code&gt;models.py&lt;/code&gt; :&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Currency&lt;/span&gt;(models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IntegerChoices):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    EUR &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, _(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;EUR&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    GBP &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;, _(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;GBP&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    USD &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;, _(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;USD&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    GBX &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;4&lt;/span&gt;, _(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;GBX&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;currency_enum_from_string&lt;/span&gt;(currency: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; Currency:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; Currency[currency]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;KeyError&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ValueError&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Unsupported currency &amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; currency)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;currency_string_from_enum&lt;/span&gt;(currency: Currency) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; str:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; Currency(currency)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;label&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Account&lt;/span&gt;(models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Model):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    user &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ForeignKey(User, on_delete&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CASCADE)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    currency &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IntegerField(choices&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;Currency&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;choices, default&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;Currency&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;EUR)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    nickname &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CharField(max_length&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;200&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    description &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;TextField(blank&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    balance &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;DecimalField(max_digits&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;12&lt;/span&gt;, decimal_places&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;, default&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    last_modified &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;DateTimeField(auto_now&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;, null&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;__str__&lt;/span&gt;(self):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; (&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;lt;Account user: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;user&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;, nickname: &amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;nickname&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;, &amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;currency: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_currency_display()&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;gt;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;value_history_per_position&lt;/span&gt;(self, from_date, to_date):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        results &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; []&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; position &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;positions&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;all():&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            results&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                (&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    position&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;pk,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    position&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;value_history_in_account_currency(from_date, to_date),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                )&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            )&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; results&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Meta&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        unique_together &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;nickname&amp;#34;&lt;/span&gt;]]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;views.py&lt;/code&gt;&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AccountsViewSet&lt;/span&gt;(viewsets&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ModelViewSet):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    permission_classes &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [permissions&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IsAuthenticated]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    serializer_class &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; AccountSerializer&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    pagination_class &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; LimitOffsetPagination&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    basename &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;account&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get_queryset&lt;/span&gt;(self) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; QuerySet[models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Account]:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;assert&lt;/span&gt; isinstance(self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;request&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;user, User)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        queryset &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Account&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;objects&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(user&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;request&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;user)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;annotate(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            positions_count&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;Count(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;positions&amp;#34;&lt;/span&gt;, distinct&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            transactions_count&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;Count(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;positions__transactions&amp;#34;&lt;/span&gt;, distinct&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; queryset&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get_serializer_context&lt;/span&gt;(self) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; Dict[str, Any]:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        context: Dict[str, Any] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; super()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_serializer_context()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;request&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;request&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        query &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; FromToDatesSerializer(data&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;request&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;query_params)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; query&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;is_valid(raise_exception&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; query&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;validated_data&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;query_data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; data&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;from_date&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;query_data&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;from_date&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                datetime&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;date&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;today() &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; datetime&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;timedelta(days&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt;),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            )&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;to_date&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;query_data&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;to_date&amp;#34;&lt;/span&gt;, datetime&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;date&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;today())&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; context&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get_serializer_class&lt;/span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; Type[&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Union[AccountEditSerializer, AccountWithValuesSerializer, AccountSerializer]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ]:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;action &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; (&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;create&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;update&amp;#34;&lt;/span&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; AccountEditSerializer&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;action &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;retrieve&amp;#34;&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; AccountWithValuesSerializer&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; AccountSerializer&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;retrieve&lt;/span&gt;(self, request, pk&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        queryset &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_queryset()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        queryset &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; queryset&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;prefetch_related(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;positions__security&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        account &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; get_object_or_404(queryset, pk&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;pk)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        serializer &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_serializer(account, context&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_serializer_context())&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; Response(serializer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;data)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;create&lt;/span&gt;(self, request, &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;args, &lt;span style=&#34;color:#f92672&#34;&gt;**&lt;/span&gt;kwargs):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        serializer &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_serializer(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            data&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;request&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;data, context&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_serializer_context()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        serializer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;is_valid(raise_exception&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;assert&lt;/span&gt; isinstance(self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;request&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;user, User)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        accounts&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;AccountRepository()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;create(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            user&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;request&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;user, &lt;span style=&#34;color:#f92672&#34;&gt;**&lt;/span&gt;serializer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;validated_data&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        headers &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_success_headers(serializer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;data)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; Response(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            serializer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;data, status&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;status&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;HTTP_201_CREATED, headers&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;headers&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;serializers.py&lt;/code&gt; :&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AccountSerializer&lt;/span&gt;(serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ModelSerializer[Account]):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    positions_count &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IntegerField()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    transactions_count &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IntegerField()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    currency &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; CurrencyField()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Meta&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        model &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Account&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        fields &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;currency&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;nickname&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;description&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;balance&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;last_modified&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;positions_count&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;transactions_count&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AccountEditSerializer&lt;/span&gt;(serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ModelSerializer[Account]):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# Currency needs to be changed from string to enum.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    currency &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; CurrencyField()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Meta&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        model &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Account&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        fields &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;currency&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;nickname&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;description&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;validate_nickname&lt;/span&gt;(self, value):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# If user was also included in the serializer then unique_together&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# constraint would be automatically evaluated, but&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# since user is not included in the serializer the validation is&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# done manually.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        request &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;context&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;request&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; request &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; hasattr(request, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user&amp;#34;&lt;/span&gt;):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            user &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; request&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;user&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; Account&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;objects&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(user&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;user, nickname&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;value)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;count() &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt; serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ValidationError(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;User already has an account with name: &amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;value&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                )&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; value&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AccountWithValuesSerializer&lt;/span&gt;(serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ModelSerializer[Account]):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    positions_count &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IntegerField()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    transactions_count &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;IntegerField()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    currency &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; CurrencyField()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    values &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;SerializerMethodField()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Meta&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        model &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Account&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        fields &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;currency&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;nickname&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;description&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;balance&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;last_modified&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;positions_count&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;transactions_count&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;values&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get_values&lt;/span&gt;(self, obj):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        from_date &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;from_date&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        to_date &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;to_date&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; obj&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;value_history_per_position(from_date, to_date)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;recommendations&#34;&gt;Recommendations&lt;/h2&gt;&lt;p&gt;Django rest framework is surprisingly well designed and provides a lot of great places for convenient customization.Whenever you are doing something a bit unusual, instead of jumping straight to stack overflow spend some time looking at the &lt;a href=&#34;codebase&#34;&gt;https://github.com/encode/django-rest-framework/tree/master&lt;/a&gt; either on github or within your editor.&lt;/p&gt;&lt;p&gt;Happy coding! If you find this article helpful, please share it and feel free to follow me on &lt;a href=&#34;https://twitter.com/attilczuk&#34;&gt;twitter&lt;/a&gt;.&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Django - growing up from SQLite to PostgreSQL</title>
       <link>https://tinystruggles.com/posts/django_db_change/</link>
       <pubDate>Sun, 11 Jul 2021 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/django_db_change/</guid>
       <description>&lt;p&gt;Django by default comes with &lt;a href=&#34;https://www.sqlite.org/about.html&#34;&gt;SQLite&lt;/a&gt; as a database.It&amp;rsquo;s a great choice for initial development and for testing,but not if you want to run multiple app servers, e.g. in a SaaS project. At some point it&amp;rsquo;s time to pick up a DB like MySQL or PostgreSQL that is more suitable for this purpose.&lt;/p&gt;&lt;p&gt;I&amp;rsquo;ve been working on my open source side project &lt;a href=&#34;https://github.com/ilonajulczuk/invertimo&#34;&gt;Invertimo&lt;/a&gt;for a while and at some point I decided tochange the db engine from SQLite to PostgreSQL. Well&amp;hellip; it wasn&amp;rsquo;t an entirely smooth transition.&lt;/p&gt;&lt;p&gt;I&amp;rsquo;m presenting you three stories of my database migration struggles with some valuable lessons learned along the way.&lt;/p&gt;&lt;h2 id=&#34;configuration-glitches&#34;&gt;Configuration glitches&lt;/h2&gt;&lt;p&gt;I don&amp;rsquo;t set up new databases like that every day, so I looked up some tutorials and followed &lt;a href=&#34;https://dev.to/jkaylight/django-rest-framework-with-postgresql-a-crud-tutorial-1l34&#34;&gt;this one&lt;/a&gt;.There was only one issue, my tests would not work due to permission issues!&lt;/p&gt;&lt;p&gt;Thanks goodness, Django has pretty good documentation for its settings, you can find relevant sections&lt;a href=&#34;https://docs.djangoproject.com/en/3.2/ref/settings/#databases&#34;&gt;here&lt;/a&gt; and &lt;a href=&#34;https://docs.djangoproject.com/en/3.2/topics/testing/overview/#the-test-database&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Aside from using a separate database, the test runner will otherwise use all of the same database settings you have in your settings file: ENGINE, USER, HOST, etc. The test database is created by the user specified by USER, so you’ll need to make sure that the given user account has sufficient privileges to create a new database on the system.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;The key takeaways are:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;when using PostgreSQL - the user needs to have Create DB permissions&lt;/li&gt;&lt;li&gt;test db is basically the same as the &amp;rsquo;normal&amp;rsquo; db, you can do very minor overrides, like change a &lt;code&gt;NAME&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;To solve the permission problem I needed to give my PostgreSQL user &lt;a href=&#34;https://dba.stackexchange.com/questions/33285/granting-a-user-account-permission-to-create-databases-in-postgresql&#34;&gt;permissions to create new databases&lt;/a&gt;:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-SQL&#34; data-lang=&#34;SQL&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;ALTER&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;USER&lt;/span&gt; myuser &lt;span style=&#34;color:#66d9ef&#34;&gt;CREATEDB&lt;/span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As a side note, I have tried a configuration like below to make my tests run with SQLite:&lt;/p&gt;&lt;p&gt;** BAD **&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;DATABASES &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;default&amp;#39;&lt;/span&gt;: {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;ENGINE&amp;#39;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;django.db.backends.postgresql&amp;#39;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;USER&amp;#39;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;mydatabaseuser&amp;#39;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;NAME&amp;#39;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;mydatabase&amp;#39;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;TEST&amp;#39;&lt;/span&gt;: {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# Will not work, since Django will take the ENGINE from&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# the value one level up.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# The bad thing is that it will not show you any warning,&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# that the configuration is invalid.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;ENGINE&amp;#39;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;django.db.backends.sqlite3&amp;#39;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;NAME&amp;#39;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;mytestdatabase&amp;#39;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        },&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But it didn&amp;rsquo;t work and I would generally not recommend that approach. I will explain the issues with that later in the article.&lt;/p&gt;&lt;p&gt;If you really needed to do that though, here is a configuration that would work:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; sys&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;test&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; sys&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;argv:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    DATABASES[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;default&amp;#39;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;ENGINE&amp;#39;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;django.db.backends.sqlite3&amp;#39;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;NAME&amp;#39;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;mydatabase&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Instead of setting overrides within the &amp;lsquo;TEST&amp;rsquo; dictionary, you could override the settings for your &amp;lsquo;default&amp;rsquo; db directly when the program is run in the context of tests.More about this solution can be found &lt;a href=&#34;https://stackoverflow.com/questions/4650509/different-db-for-testing-in-django&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;h2 id=&#34;problems-with-fixtures&#34;&gt;Problems with fixtures&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;m not a huge fan of &lt;a href=&#34;https://docs.djangoproject.com/en/3.2/topics/testing/tools/#fixture-loading&#34;&gt;database test fixtures&lt;/a&gt;,but they have their good use cases and django supports them pretty well. They can be a pain to maintain (hello, database changes!) and they canmake your tests slow if you load too much data.&lt;/p&gt;&lt;p&gt;In my Invertimo projectI&amp;rsquo;ve been using one fixture for things such as Stock Exchanges (otherwise setting them up in tests would require quite a lot of boilerplate).&lt;/p&gt;&lt;h3 id=&#34;natural-keys-stumble&#34;&gt;Natural keys stumble&lt;/h3&gt;&lt;p&gt;It was unclear to me why, but my old fixture that worked with SQLite wouldn&amp;rsquo;t work with PostgreSQL now. Obviously, I couldn&amp;rsquo;t let my tests to keep failing and I decided that it will be easier for me torecreate the data and create a new fixture.&lt;/p&gt;&lt;p&gt;Django implements two very convenient methods for dealing with fixtures: &lt;code&gt;loaddata&lt;/code&gt; and &lt;code&gt;dumpdata&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;It seems like a common source of problems is the django &lt;code&gt;contenttypes&lt;/code&gt; that are autogenerated and &amp;lsquo;shouldn&amp;rsquo;t be serialized&amp;rsquo;. Well, by default they are, unless you exclude them or&amp;hellip; do a natural key trick.&lt;/p&gt;&lt;p&gt;Like I did, when I created my application:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python manage.py dumpdata --natural-primary &amp;gt; finance/fixtures/exchanges.json&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Well,  you shouldn&amp;rsquo;t just use a flag, because someone on the internet suggested doing that. It can lead to another types of problems.&lt;/p&gt;&lt;p&gt;Do better than me, read about &lt;a href=&#34;https://docs.djangoproject.com/en/3.2/topics/serialization/#natural-keys&#34;&gt;natural keys and serialization in django&lt;/a&gt; before thinking about using them.&lt;/p&gt;&lt;p&gt;So, I created my new fixtures, they managed to load with PostgreSQL, I found another bug that was related to db differences which I will talk about later and all was fine for a while.&lt;/p&gt;&lt;p&gt;But then I added some more features and added more tests and I suddenly started experiencing weird integrity errors across tests. I don&amp;rsquo;t know how the django tests areinteracting with the db and I didn&amp;rsquo;t feel like reverse engineering the code (it&amp;rsquo;s a weekend side project after all and I have other things to do too&amp;hellip;), but believe meit was very annoying.&lt;/p&gt;&lt;p&gt;My stack overflow searches were not yielding many results, but I couldn&amp;rsquo;t give up of course! The test with a fixture was rather important and I didn&amp;rsquo;tfeel like redoing the db setup from scratch in the test. I ended diving into my fixtures and analyzing both the errors I have been getting. All the errors wererelated to loading the serialized User and failing uniqueness constraint on primary keys.&lt;/p&gt;&lt;p&gt;I found something a bit odd - my serialized user didn&amp;rsquo;t have a &amp;lsquo;pk&amp;rsquo; field.&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;model&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;auth.user&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;fields&amp;#34;&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; {&lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;password&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pbkd1234zE9lKo=&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;last_login&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2021-05-23T12:28:17.174Z&amp;#34;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;is_superuser&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;username&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;leethacker&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;first_name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;last_name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;email&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;leethacker@gmail.com&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;is_staff&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;is_active&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Odd, right? Well, I made it this way by using the &lt;code&gt;--natural-primary&lt;/code&gt; flag.Adding a fake &lt;code&gt;&amp;quot;pk&amp;quot;: 122&lt;/code&gt; manually to my fixture solved my problem. Oof, believe me that was a pretty frantic debugging session.&lt;/p&gt;&lt;p&gt;Note for my future self, ditch the contenttypes instead of using &lt;code&gt;--natural-primary&lt;/code&gt; for this specific use case:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python manage.py dumpdata --exclude&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;contenttypes &amp;gt; my_data.json&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Another option would be not using fixtures at all and save myself from all associated complexity of serializing and deserializing data.&lt;/p&gt;&lt;h3 id=&#34;is-postgresql-just-so-much-slower-than-sqlite&#34;&gt;Is PostgreSQL just so much slower than SQLite?&lt;/h3&gt;&lt;p&gt;When I did my initial jump from PostgreSQL to SQLite (and re-dumped my fixture) my tests got significantly slower. I blamed it on PostgreSQL being &amp;lsquo;heavy-weight&amp;rsquo; compared to SQLite.Don&amp;rsquo;t people like to switch their db to SQLite to speed up their test?&lt;/p&gt;&lt;p&gt;Well, my tests were awfully slow with PostgreSQL. I figured out why when I was having problems with my fixture integrity errors.My fixture was awfully, awfully long. Long enough for github not to want to show me the diff - this could have raised some red flags, but it didn&amp;rsquo;t.&lt;/p&gt;&lt;p&gt;Turns out I accidentally dumped multi-year daily stock price history of multiple stocks to a json text file.No surprise that my tests would take 10s+ when parsing such data. I cut it out and sped up my tests about 10x.&lt;/p&gt;&lt;p&gt;A tip for my future self:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Evaluate the autogenerated fixtures critically for unnecessary data.&lt;/p&gt;&lt;/blockquote&gt;&lt;h3 id=&#34;db-representation-differences-can-cause-application-bugs&#34;&gt;DB representation differences can cause application bugs&lt;/h3&gt;&lt;p&gt;SQL Databases share some common features, but also have their differences. Some obvious ones are query syntax data types and performance differences.Here is a good &lt;a href=&#34;https://www.enterprisedb.com/blog/postgresql-vs-mysql-360-degree-comparison-syntax-performance-scalability-and-features&#34;&gt;comparison between MySQL and PostgreSQL&lt;/a&gt;.The more RAW SQL or custom DB features your django app is using the harder a DB switch is going to be.&lt;/p&gt;&lt;p&gt;My app seemed to very simple when it comes to using the SQL database, no custom SQL, no fancy DB features, but I managed to hit a complex issue anyways!&lt;/p&gt;&lt;p&gt;Invertimo has a feature when user can import their stock transactions via a CSV file. I use pandas to load and manipulate that.&lt;/p&gt;&lt;p&gt;Here are some code snippets that contained a very specific bug:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;transactions_data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pd&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;read_csv(filename)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;transaction_record &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; transactions_data_clean&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;iloc[x]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I get my float values and convert them to decimals, because this is how they are stored in the database:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;quantity &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; transaction_record[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Quantity&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;to_decimal&lt;/span&gt;(pd_f):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; pd_f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;astype(decimal&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Decimal)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;accounts&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;AccountRepository()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;add_transaction(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    account,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    isin&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;isin,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    exchange&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;exchange,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    executed_at&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;executed_at,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    quantity&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;to_decimal(quantity),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    price&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;to_decimal(price),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    transaction_costs&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;transaction_costs,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    local_value&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;to_decimal(local_value),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    value_in_account_currency&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;to_decimal(value_in_account_currency),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    total_in_account_currency&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;to_decimal(total_in_account_currency),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    order_id&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;order_id,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I avoid adding duplicate transactions, by using Django&amp;rsquo;s get_or_create that should avoid adding an instance of the model if there is an instance with exactly same values already:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;transaction, created &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Transaction&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;objects&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_or_create(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    executed_at&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;executed_at,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    position&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;position,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    quantity&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;quantity,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    price&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;price,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    transaction_costs&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;transaction_costs,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    local_value&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;local_value,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    value_in_account_currency&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;value_in_account_currency,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    total_in_account_currency&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;total_in_account_currency,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    order_id&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;order_id,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Luckily I had tests for this functionality, unluckily the tests started failing after I switched the DB from SQLite to PostgreSQL.According to my tests, my duplicate prevention logic stopped working. In other words, I could end up with duplicate transactions now.&lt;/p&gt;&lt;p&gt;I narrowed down the issue to a problem with &lt;code&gt;get_or_create&lt;/code&gt;. The only thing that changed was the DB, what could there be wrong?&lt;/p&gt;&lt;p&gt;It turns out that if I tried to import the same transaction twice a price in database vs a new one in the &lt;code&gt;get_or_create&lt;/code&gt; would nownot be identical and another transaction would be created, this way creating a duplicate.&lt;/p&gt;&lt;p&gt;Why would a transaction not be deduplicated with &lt;code&gt;get_or_create&lt;/code&gt; if the initializing values were identical? My best educated guess is that thedecimal values would not match. Could it be that pandas &lt;code&gt;astype&lt;/code&gt; conversion was problematic?&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;to_decimal&lt;/span&gt;(pd_f):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; decimal&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Decimal(pd_f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;astype(decimal&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Decimal))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This code worked perfectly fine with &lt;code&gt;get_or_create&lt;/code&gt; while using SQLite, but with PostgreSQL it was causing transaction duplicates.&lt;/p&gt;&lt;p&gt;The fix was to convert the Decimal argument to string first:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;to_decimal&lt;/span&gt;(pd_f):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; decimal&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Decimal(pd_f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;astype(str))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Most likely it was some type of a precision issue caused by conversion from floats, similar to this:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;In [&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]: price &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;4.35&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;In [&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;]: decimal&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Decimal(decimal&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Decimal(price))&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Out[&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;]: Decimal(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;4.3499999999999996447286321199499070644378662109375&amp;#39;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;In [&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;]: decimal&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Decimal(str(price))&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Out[&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;]: Decimal(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;4.35&amp;#39;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Converting the argument to string first got rid of the &amp;lsquo;imprecision&amp;rsquo; of the float and decimal stored in the db.Different databases don&amp;rsquo;t sore values like Decimal the same way, there are implementation differences big enough to change mycode from working to not working.&lt;/p&gt;&lt;p&gt;The lesson from here is to write good tests and use the same database engine for tests and production code.This bug was tricky and non-intuitive. Tools like mypy that helps withtypes in python could not save me here. I was lucky my tests caught it or otherwise I would have happily shipped the error in production.&lt;/p&gt;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;&lt;p&gt;Database switch can be a quite painful and a time consuming operation, but it can be done. It might be easier tojust start your app with the target database in mind to save yourself future trouble.&lt;/p&gt;&lt;p&gt;Otherwise make sure that you buffer some time to fix up the issues that might come up in the process.&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Shooting yourself in a foot with django and recovering from it</title>
       <link>https://tinystruggles.com/posts/django_slowness_traps/</link>
       <pubDate>Fri, 19 Feb 2021 20:45:59 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/django_slowness_traps/</guid>
       <description>&lt;p&gt;I have recently built an API (for my side project &lt;a href=&#34;https://redeal.app&#34;&gt;Redeal&lt;/a&gt;) that turned out extremely slow. I knew that what I was building wasn&amp;rsquo;t supposed to be super fast or optimal and I was building it with python and django which aren&amp;rsquo;t really known for their speed, but I haven&amp;rsquo;t expected such atrocious performance:&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/ohmygod_so_slow.png&#34; alt=&#34;30s to load data&#34;&gt;&lt;/p&gt;&lt;p&gt;Yup, you are seeing it right, it was taking about &lt;strong&gt;30 seconds&lt;/strong&gt; for all the API calls finish. Each individual call was slow and there were also multiple of them.In this post I will tell you what I did to make it more than 200x faster.&lt;/p&gt;&lt;p&gt;The problem I was facing wasn&amp;rsquo;t something fundamental to the tools I was using. I didn&amp;rsquo;t have to rewrite my app to a faster language like Go, or to ditch my SQL database.&lt;/p&gt;&lt;p&gt;I was pretty sure that the slowness in this case was self inflicted.&lt;/p&gt;&lt;p&gt;Django is one of my go to tools for building any sorts of web apps. I love the batteries included, so that I don&amp;rsquo;t have to reinvent the wheel over and over.The downside is that sometimes it&amp;rsquo;s way to easy to misuse the solution and end up with the app being very slow.&lt;/p&gt;&lt;h2 id=&#34;where-to-even-start&#34;&gt;Where to even start?&lt;/h2&gt;&lt;p&gt;My first instinct when I see a problem is to come up with some ideas straight off the bat.&lt;/p&gt;&lt;p&gt;Here is something that crossed my mind:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Should I use async generators with concurrent promises to parallelize my API calls?&lt;/em&gt; &lt;em&gt;Should I add backend caching?&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;It would probably be fun to implement, but this is the wrong approach to performance debugging and at best would optimize my API calls to take about 2 seconds.Which would be still way to slow for my standards. I believed the problem was on the backend side.&lt;/p&gt;&lt;p&gt;The reason behind why there were so many API calls was that I was using an API with pagination and I was fetching next pages after previous ones completed.To simplify debugging I decided to make my pages huge so that I would only need one API call.&lt;/p&gt;&lt;p&gt;Since I was using Django Rest Framework with the custom paginator class, it was pretty easy to override and I simplified my problem to just one big API call. I could have also change the pagination options from the JavaScript side, but since in this case I controlled both, I chose to make changes on the backend side.&lt;/p&gt;&lt;p&gt;With the pagination change I ended up having just one slow request instead of many:&lt;img src=&#34;https://tinystruggles.com/oh_still_slow.png&#34; alt=&#34;multiple_seconds&#34;&gt;&lt;/p&gt;&lt;p&gt;First hint: overall it was significantly faster than smaller but sequential requests.&lt;/p&gt;&lt;h3 id=&#34;django-debug-toolbar&#34;&gt;Django Debug Toolbar&lt;/h3&gt;&lt;p&gt;I usually start debugging performance of django web apps by looking at what &lt;a href=&#34;https://github.com/jazzband/django-debug-toolbar&#34;&gt;Django Debug Toolbar&lt;/a&gt; can tell me.Django Debug Toolbar is a configurable set of panels that display various debug information about the current request/response and when clicked, display more details about the panel&amp;rsquo;s content.You can read how to set it up &lt;a href=&#34;https://django-debug-toolbar.readthedocs.io/en/latest/installation.html&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;So, since now I had just a single, but very slow API call, I opened it in it&amp;rsquo;s own tab.Since I was using Django Rest Framework, the JSON was rendered nicely and I could also see the Debug Toolbar on the side.&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/debug_toolbar_django_rest_framework.png&#34; alt=&#34;Toolbar&#34;&gt;&lt;/p&gt;&lt;p&gt;The things that immediately got my attention:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Time (5s+)&lt;/li&gt;&lt;li&gt;SQL (296 queries!) - each query individually was fast and&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;When you click at the navigation in the toolbar you can see more info in the panel.&lt;img src=&#34;https://tinystruggles.com/sql_queries.png&#34; alt=&#34;sql&#34;&gt;There I saw that there were extremely many similar to each other queries that were sequential and that they were taking a very long time, because of that.&lt;/p&gt;&lt;p&gt;Another panel that I investigated was the Profiling panel that shows how long did functions in the call stack take.&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/profiling.png&#34; alt=&#34;profiling&#34;&gt;&lt;/p&gt;&lt;p&gt;The profiling panel pointed me to two problems:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;get_stats_from_queryset&lt;/code&gt; taking 1.7s&lt;/li&gt;&lt;li&gt;&lt;code&gt;rest_framework/serializers.py&lt;/code&gt; taking 1.93s&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Ouch.&lt;/p&gt;&lt;h3 id=&#34;reason-for-the-slowness&#34;&gt;Reason for the slowness&lt;/h3&gt;&lt;p&gt;The truth is that I anticipated that the &lt;code&gt;get_stats_from_queryset&lt;/code&gt; would be slow when I was writing it.I knew that I would have to optimize it, because I was writing some fairly naive code. It was howevera bit surprising that the serializer code was also very slow.&lt;/p&gt;&lt;p&gt;Let&amp;rsquo;s dig into my specific problem.I&amp;rsquo;ve been using django ORM and my models where more less as follows:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Listing&lt;/span&gt;(models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Model):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    url &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;URLField(unique&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    description &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;TextField()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Price&lt;/span&gt;(models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Model):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    listing &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ForeignKey(Listing,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                on_delete&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CASCADE,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                related_name&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;prices&amp;#39;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    created &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;DateTimeField(auto_now_add&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    value &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;DecimalField(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        max_digits&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;, decimal_places&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;, null&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Meta&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ordering &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;-created&amp;#39;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ve been tracking listings over time and I was maintaining the history of price changes.What I also wanted in my API was price statistics for the subset of listings matching given criteria.That was computed in the function &lt;code&gt;get_stats_from_queryset&lt;/code&gt; which result would later be added to the response theDjango Rest Framework &lt;code&gt;ListAPIView&lt;/code&gt; would return.&lt;/p&gt;&lt;p&gt;Here is the listing of how the records were prepared to later get statistics:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get_stats_from_queryset&lt;/span&gt;(queryset):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    stats &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {}&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# Within the queryset, map each property to the last price.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    last_prices &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; []&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; listing &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; queryset:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        prices &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; listing&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;prices&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;order_by(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;-created&amp;#39;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; prices:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            last_price &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; prices[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;]&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;value&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            last_prices&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append(last_price)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And here is the simplified code of the serializers:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; rest_framework &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; serializers&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PriceSerializer&lt;/span&gt;(serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ModelSerializer):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Meta&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        model &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Price&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        fields &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;created&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;value&amp;#39;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ListingSerializer&lt;/span&gt;(serializers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ModelSerializer):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    prices &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; PriceSerializer(many&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Meta&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        model &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Listing&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        fields &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;url&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;description&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;prices&amp;#39;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So, this isn&amp;rsquo;t anything super special, two models with a ForeignKey 1 to Many relationship.&lt;/p&gt;&lt;p&gt;The majority of the slowness was coming from a deluge of simple queries that were comingfrom both the serializers and the custom function that computed the stats.&lt;/p&gt;&lt;h3 id=&#34;making-things-fast&#34;&gt;Making things fast&lt;/h3&gt;&lt;p&gt;At this point, I knew that the best way to speed up this API would be to reduce the amount of SQL queries.&lt;/p&gt;&lt;p&gt;I have briefly considered just writing my SQL query from scratch, but it would be surprisingly complicated to dofor selecting the value of the latest price (I have walked through it and there would be at least two joins necessary).&lt;/p&gt;&lt;p&gt;Luckily, this is not the new problem for the django users and django offers really good tools to optimize its ORM queries.You could use &lt;a href=&#34;https://docs.djangoproject.com/en/3.1/ref/models/querysets/#select-related&#34;&gt;select related&lt;/a&gt; or &lt;a href=&#34;https://docs.djangoproject.com/en/3.1/ref/models/querysets/#prefetch-related&#34;&gt;prefetch related&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;In this case I needed to use &lt;code&gt;prefetch related&lt;/code&gt;, because I needed to fetch multiple related objects.I made a small tweak to the &lt;code&gt;get_queryset&lt;/code&gt; method in my view that looked like this:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ListingView&lt;/span&gt;(generics&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ListAPIView):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get_queryset&lt;/span&gt;(self):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; queryset&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;filter(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt; (my custom filters) &lt;span style=&#34;color:#f92672&#34;&gt;..&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            )&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;prefetch_related(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;prices&amp;#39;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I navigated back to my django rest framework API page and checked the performance:&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/queries.png&#34; alt=&#34;still slow&#34;&gt;&lt;/p&gt;&lt;p&gt;It improved significantly! It changed from 5.7s to ~2.5s. But it was still too slow. There were still too many queries!The problem was that the &lt;code&gt;get_stats_from_queryset&lt;/code&gt; was using  &lt;code&gt;listing.prices.order_by(&#39;-created&#39;)&lt;/code&gt; which had custom ordering and because of thatit wasn&amp;rsquo;t taking advantage of the prefetch!&lt;/p&gt;&lt;p&gt;This could be addressed in multiple ways.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;By using specifying the ordering in the &lt;code&gt;Meta&lt;/code&gt;&lt;/li&gt;&lt;/ol&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Price&lt;/span&gt;(models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Model):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    listing &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ForeignKey(Listing,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                on_delete&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CASCADE,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                related_name&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;prices&amp;#39;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    created &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;DateTimeField(auto_now_add&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    value &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; models&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;DecimalField(&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        max_digits&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;, decimal_places&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;, null&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Meta&lt;/span&gt;:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ordering &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;-created&amp;#39;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(that I was already doing here)&lt;/p&gt;&lt;ol start=&#34;2&#34;&gt;&lt;li&gt;By using&lt;/li&gt;&lt;/ol&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;myqueryset&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;prefetch_related(Prefetch(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;prices&amp;#39;&lt;/span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    queryset&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;Price&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;objects&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;order_by(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;-created&amp;#39;&lt;/span&gt;)))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When either one 1. or 2. is used and if we drop the &lt;code&gt;order_by&lt;/code&gt; from prices (replacing it with &lt;code&gt;prices = listing.prices.all()&lt;/code&gt;), the method startstaking advantage of the &lt;code&gt;prefetch_related&lt;/code&gt; while still remaining correct. You can both inspect the SQL queries to make sure and check if your unit tests still pass!&lt;/p&gt;&lt;p&gt;So, here we have it, now without the deluge of the SQL queries:&lt;/p&gt;&lt;p&gt;&lt;img src=&#34;https://tinystruggles.com/improved.png&#34; alt=&#34;finally&#34;&gt;&lt;/p&gt;&lt;p&gt;It&amp;rsquo;s still not very fast, overall the profile shows about 400ms to generate the response. Big chunk of that time comes from the rendering the fancy HTML viewfrom the Django Rest Framework that we can see on the following CPU profile:&lt;img src=&#34;https://tinystruggles.com/improved_profile.png&#34; alt=&#34;improved profile&#34;&gt;&lt;/p&gt;&lt;p&gt;When we request the API in the JSOn format it takes on average 230-250ms.&lt;/p&gt;&lt;h3 id=&#34;making-it-even-faster&#34;&gt;Making it even faster&lt;/h3&gt;&lt;p&gt;Taking 250ms to fetch about 150 records and also compute a bunch of statistics about them it&amp;rsquo;s not a bad result. But in my mind it was still pretty slow.I further made it about 40-50% faster.&lt;/p&gt;&lt;p&gt;The thing was that I didn&amp;rsquo;t actually need all the prices in my API, I just needed the last price for each listing. With this mind, I denormalized my dband added a new field called &lt;code&gt;last_price&lt;/code&gt; on each listing, which would be added to each listing. That wouldn&amp;rsquo;t be a problem because I would generally create and update the listing and prices within one method and a single transaction.I wrote a data migration in django that automatically filled in the new database field based on the prices.&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://docs.djangoproject.com/en/3.1/howto/writing-migrations/&#34;&gt;https://docs.djangoproject.com/en/3.1/howto/writing-migrations/&lt;/a&gt;&lt;/p&gt;&lt;p&gt;My migration looked similar to:&lt;/p&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; django.db &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; migrations, models&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fill_last_price&lt;/span&gt;(apps, schema_editor):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Listing &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; apps&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_model(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;myapp&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;Listing&amp;#39;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; listing &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; Listing&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;objects&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;all():&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        prices &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; listing&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;prices&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;order_by(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;-created&amp;#39;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; prices:&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            listing&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;last_price &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; prices[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;]&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;value&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        listing&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;save()&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Migration&lt;/span&gt;(migrations&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Migration):&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    dependencies &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        (&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;myapp&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;0004_listing_last_price&amp;#39;&lt;/span&gt;),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ]&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    operations &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        migrations&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;RunPython(fill_last_price),&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With that I didn&amp;rsquo;t have to use &lt;code&gt;prefetch_related&lt;/code&gt; and I also could simplify my serializers. It generally made my code quite a bit simpler and it was a useful tradeoff for me to make.&lt;/p&gt;&lt;p&gt;I went from about 30s of API calls to ~140ms.&lt;img src=&#34;https://tinystruggles.com/faster_now.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;&lt;p&gt;Which is more than 200x speedup for the same exact functionality.&lt;/p&gt;&lt;h3 id=&#34;what-went-well&#34;&gt;What went well&lt;/h3&gt;&lt;p&gt;I found the issue in the development stage and not from the customer complaints. What helped here was that I was developing with a fairly realistic dataset. If instead of 150 records, I was using 3 in development I could have completely missed the issue. So the tip here would be to have good fixtures or development data.&lt;/p&gt;&lt;p&gt;The second thing that went well is that I used a disciplined approach to performance improvements starting with profiling. If I have jumped to some random idea like, concurrent promises, or even blindly applying caching I would end up with much worse results.&lt;/p&gt;&lt;p&gt;The last thing that I want to acknowledge is that django has a rich ecosystem and great tooling that made this process fairly easy. Yes, I shot myself in the foot at first. But django helped me recover.I would recommend any django enthusiast to use the &lt;a href=&#34;https://django-debug-toolbar.readthedocs.io/en/latest/&#34;&gt;django debug toolbar&lt;/a&gt;. And when it comes to the django database optimization, there is also very good &lt;a href=&#34;https://docs.djangoproject.com/en/3.1/topics/db/optimization/&#34;&gt;documentation&lt;/a&gt; worth reading.&lt;/p&gt;&lt;p&gt;If you liked this article please share it further and follow me on &lt;a href=&#34;https://twitter.com/attilczuk&#34;&gt;twitter&lt;/a&gt;!&lt;/p&gt;</description>
     </item>
   
     <item>
       <title>Revived</title>
       <link>https://tinystruggles.com/posts/revived/</link>
       <pubDate>Sat, 13 Feb 2021 15:58:36 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/revived/</guid>
       <description>&lt;p&gt;I got too busy with a new job and living in a different country. But I love writing and I am very happy to start writing in &lt;a href=&#34;tinystruggles.com&#34;&gt;tinystruggles.com&lt;/a&gt; again.&lt;/p&gt;&lt;p&gt;What have I been up to in last couple of years? I have joined Google in Dublin, so new job, new country, new friends, new hobbies. Lot&amp;rsquo;s of changes and I got pretty absorbed by them.&lt;/p&gt;&lt;p&gt;Since that time (late 2015) I have updated this blog literally once and in 2017. I lost the habit of consistent blogging and I also focused my tech related activities around work. Obviously not working too much, I was pretty serious about maintaining my work life balance, but I was learning much more about distributed systems and databases instead of for example web development. I did work on bunch of technical side projects, but with Google&amp;rsquo;s copyright, etc. I felt reluctant to make any of it public.&lt;/p&gt;&lt;p&gt;At the end of 2019 I considered learning more about game dev and got myself a pretty powerful laptop. Nice lenovo thinkpad with a graphic card, bells and whistles. It was a nice upgrade from my at that time very old personal laptop and somehow encouraged me to start many more non-work related side projects. I also quickly decided that game dev is not my thing, but I entered 2020 enthusiastic about doing some indie-hacking. Was very productive for me when it comes to side projects, there was much less travel due to the pandemic and I finally got a really nice work from home setup.&lt;/p&gt;&lt;p&gt;I haven&amp;rsquo;t shared anything about them on this blog though, because I lost a good way to update it. Due to the fact that the blog was effectively abandoned (and not properly backed up!) and was using some obscure static site generation tech,I couldn&amp;rsquo;t simply pick it up and add to it. I had to actually revive it. Also, the server I was hosting it on really didn&amp;rsquo;t enjoy the update from ubuntu 14 to ubuntu 20 and broke terribly. The static site generator got migrated to python3 and was somehow also not working. It just became a terrible mess. I could maybe escape it, but it would surely take a lot of time and I wasn&amp;rsquo;t feeling like it. I just set up a new server and new blog setup to give myself a fresh start. The old posts are still accessible through &lt;a href=&#34;https://tinystruggles.com/old&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;</description>
     </item>
   
     <item>
       <title></title>
       <link>https://tinystruggles.com/posts/business_lessons_from_watchlimits/</link>
       <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/business_lessons_from_watchlimits/</guid>
       <description>&lt;p&gt;Marketing&lt;/p&gt;&lt;p&gt;Marketing to random strangers is actually really hard.&lt;/p&gt;&lt;p&gt;People who I talked to or interacted with are most willing to help. Build in public community on twitter. Wannabe entrepreneur folks.&lt;/p&gt;&lt;p&gt;Random post on twitter productivity subreddits not effective.&lt;/p&gt;&lt;p&gt;Getting good content for SEO not easy, hard to market productivity related content or at least I’m a bit at loss. Technical content seems easier.&lt;/p&gt;&lt;p&gt;How could I have credibility for productivity/life content?&lt;/p&gt;&lt;p&gt;Tools&lt;/p&gt;&lt;p&gt;canvaloomtinytestimonial&lt;/p&gt;</description>
     </item>
   
     <item>
       <title></title>
       <link>https://tinystruggles.com/posts/too_many_db_queries/</link>
       <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
       
       <guid>https://tinystruggles.com/posts/too_many_db_queries/</guid>
       <description></description>
     </item>
   
 </channel>
</rss>
