CategoriesBook Reviews

Book Review: Digital Minimalism by Cal Newport

Recently, I read Digital Minimalism and started implementing some of its ideas in my life. In this post, I want to share my perspective on the book and the effects it has had in my life.

Part 1: Foundations

The main argument behind the reasoning in the book is that many people have been convinced to use technology in ways that detract from their happiness and values. Building on the philosophies of Aristotle, Henry David Thoreau, and others, Cal argues that we can find happiness through skilled, focused work and pursuing our deeply held values.

The way to achieve this is to take a minimalist approach to digital technology. That means dropping or modifying our usage of the apps, services, and devices that fail to provide the most value for our time.

Digital Minimalism

A philosophy of technology use in which you focus your online time on a small number of carefully selected and optimized activities that strongly support things you value, and then happily miss out on everything else.

Digital Minimalism, 28

This can be contrasted with a maximalist approach, which says that anything that provides some value should be included.

There are three principles Cal presents in support of this philosophy:

  1. Clutter is costly.
  2. Optimization is important.
  3. Intentionality is satisfying.

The explanations behind these principles were one of the most interesting parts of the book, and there isn’t room here to give it justice—so I will leave that for you to read!

The Digital Declutter

To be minimal requires some deep thinking about what value we actually get out of the services we use. For a quick example, some of the value that I get out of Facebook is staying connected with my family. Perhaps by commenting on my sister’s post about my niece and nephew. However, that value is probably better supported by calling my sister. The connection is more meaningful, and it’s not diluted by the mindless scrolling that Facebook often draws me into.

Cal recommends doing a month-long digital declutter to start that process. While in the middle of everything, it can be difficult to evaluate what actually works for us. This is especially true because many apps are designed specifically to grab our attention for as long as possible. Simply saying “I won’t use ______ as much” isn’t likely to last very long.

So, it’s best to start with a clean slate and put everything down for a month. Note that this covers only “optional technologies,” which we have to decide for ourselves. Cal suggests this heuristic: “consider the technology optional unless its temporary removal would harm or significantly disrupt the daily operation of your professional or personal life” (page 64).

During that month-long declutter, the goal is to find and reconnect with satisfying, meaningful activities. Then, at the end, when it’s time to start reintroducing what had been cut out, we can make decisions about whether there’s room in our lives for that, after all.

It’s not all black and white, though. A lot of times all we need to do is optimize our use of a service in order to get what we want out of it without being distracted by its lower-value parts.

Part 2: Practices

The remainder of the book focuses on various qualities of a good life, and practices we can do to obtain those qualities. The exact qualities—solitude, deep conversation over shallow connection, and high-quality leisure—are debatable, but the evidence Cal presents is convincing.

As I have started to implement some of these practices in my own life, I can say that they have been effective for me. The existence of this blog is evidence of that. Prior to doing my digital declutter, I would spend hours reading articles on my phone. Sure, some of those articles had useful information for me, but there were better ways for me to learn new things. Now I use Android’s digital wellbeing tools to limit my browser to five minutes a day, just to look up something quick when I need it, which has led to having time for writing of my own.

Conclusion

I still have room to grow in my practice as a digital minimalist, but I can wholeheartedly recommend Cal Newport’s book. Even if you decide that the philosophy isn’t for you, I think anyone can benefit from thinking about the questions it raises about our relationship with the technology that has become such a large part of our lives.

I’d love to hear your perspective on this philosophy! Especially if you have tried or are trying digital minimalism. I’m always looking for ways to continue growing.

CategoriesPythonTesting

Xonsh + Mut.py: Filtering Mut.py’s Output

Mut.py is a useful tool for performing mutation testing on Python programs. If you want to learn more about that, see my blog post over on the PyBites blog. In short, mutation testing helps us test our tests to make sure that they cover the program completely by making small changes to the code and then rerunning the tests for each change.

That’s useful information, but sometimes the output can be a bit too much. As an example, let’s set up a small program and some tests.

#example.py
import math

def check_prime(number):
    if number < 2: return False

    for i in range(2, int(math.sqrt(number)) + 1):
            if number % i == 0:
                break
    else:
        return True

    return False
#test_example.py
import pytest

from example import check_prime

def test_prime():
    assert check_prime(7)

The test passes! However, it should be obvious that simply making sure that our function can tell that 7 is prime isn’t enough to cover all its functionality. Let’s see what mut.py has to say about this.

$ mut.py --target example --unit-test test_example --runner pytest                                                                                          
[*] Start mutation process:
   - targets: example
   - tests: test_example
[*] 1 tests passed:
   - test_example [0.55317 s]
[*] Start mutants generation and execution:
   - [#   1] AOR example: [0.06451 s] survived
   - [#   2] AOR example: [0.05991 s] survived
   - [#   3] BCR example: [0.06143 s] survived
   - [#   4] COI example: [0.11853 s] killed by test_example.py::test_prime
   - [#   5] COI example: [0.11193 s] killed by test_example.py::test_prime
   - [#   6] CRP example: [0.06105 s] survived
   - [#   7] CRP example: [0.06164 s] survived
   - [#   8] CRP example: [0.06224 s] survived
   - [#   9] CRP example: [0.11637 s] killed by test_example.py::test_prime
   - [#  10] ROR example: [0.11332 s] killed by test_example.py::test_prime
   - [#  11] ROR example: [0.06272 s] survived
   - [#  12] ROR example: [0.11869 s] killed by test_example.py::test_prime
[*] Mutation score [1.67688 s]: 41.7%
   - all: 12
   - killed: 5 (41.7%)
   - survived: 7 (58.3%)
   - incompetent: 0 (0.0%)
   - timeout: 0 (0.0%)

Looks like there’s still a lot of work to be done. Seven out of the twelve mutants survived the test, but this doesn’t tell us anything about where the coverage is lacking. Let’s try adding the -m flag to mut.py.

mut.py --target example --unit-test test_example --runner pytest -m                                                                                       
[*] Start mutation process:
   - targets: example
   - tests: test_example
[*] 1 tests passed:
   - test_example [0.57480 s]
[*] Start mutants generation and execution:
   - [#   1] AOR example: 
--------------------------------------------------------------------------------
   2: 
   3: def check_prime(number):
   4:     if number < 2:
   5:         return False
-  6:     for i in range(2, int(math.sqrt(number)) + 1):
+  6:     for i in range(2, int(math.sqrt(number)) - 1):
   7:         if number % i == 0:
   8:             break
   9:     else:
  10:         return True
--------------------------------------------------------------------------------
[0.07507 s] survived
   - [#   2] AOR example: 
--------------------------------------------------------------------------------
   3: def check_prime(number):
   4:     if number < 2:
   5:         return False
   6:     for i in range(2, int(math.sqrt(number)) + 1):
-  7:         if number % i == 0:
+  7:         if number * i == 0:
   8:             break
   9:     else:
  10:         return True
  11:     
--------------------------------------------------------------------------------
[0.17719 s] survived
   - [#   3] BCR example: 
--------------------------------------------------------------------------------
   4:     if number < 2:
   5:         return False
   6:     for i in range(2, int(math.sqrt(number)) + 1):
   7:         if number % i == 0:
-  8:             break
+  8:             continue
   9:     else:
  10:         return True
  11:     
  12:     return False
--------------------------------------------------------------------------------
[0.05884 s] survived
   - [#   4] COI example: 
--------------------------------------------------------------------------------
   1: import math
   2: 
   3: def check_prime(number):
-  4:     if number < 2:
+  4:     if not (number < 2):
   5:         return False
   6:     for i in range(2, int(math.sqrt(number)) + 1):
   7:         if number % i == 0:
   8:             break
--------------------------------------------------------------------------------
[0.11854 s] killed by test_example.py::test_prime
   - [#   5] COI example: 
--------------------------------------------------------------------------------
   3: def check_prime(number):
   4:     if number < 2:
   5:         return False
   6:     for i in range(2, int(math.sqrt(number)) + 1):
-  7:         if number % i == 0:
+  7:         if not (number % i == 0):
   8:             break
   9:     else:
  10:         return True
  11:     
--------------------------------------------------------------------------------
[0.11615 s] killed by test_example.py::test_prime
[SNIP!]

Suddenly, it’s too much information!

We get the mutation and the context, which can help us pinpoint where tests need to be improved, but since even mutations that were killed show up here, it’s hard to tell at a glance which ones are safe to ignore and which ones to pay attention to.

Let’s pause for a moment here and talk about xonsh’s Callable Aliases. Xonsh, like Bash, has the ability to add aliases for common commands. Unlike Bash, xonsh’s aliases are also the method we can use to access Python functions from subprocess mode.

Aliases are stored in a mapping similar to a dictionary called, aptly, aliases. So we can add an alias by setting a key.

aliases["lt"] = "ls --human-readable --size -1 -S --classify"

Callable aliases extend this idea to form a bridge between a Python function and subprocess mode. Normally, to use anything from Python in subprocess mode would require special syntax. Useful, but limited.

We can define a callable alias just like any Python function. Since our goal is to filter out some of the noise in mut.py’s output, let’s get started on that.

A callable alias can be passed the arguments from the command (as a list of strings), stdin, stdout, and a couple other more obscure values. Our function will need stdin, which means args will also be defined—xonsh determines what values to pass in based on argument position, not the name.

Here’s how to register the alias with xonsh:

#~/.xonshrc
def _filter_mutpy(args, stdin=None):
    if not stdin:
        return "No input to filter"

aliases["filter_mutpy"] = _filter_mutpy
$ filter_mutpy
No input to filter

Success! When called with no stdin, there’s nothing for our function to parse. Xonsh accepts a string as the return value, which is appended to stdout. There are two more optional values that could also be used: stderr and a return code. To use them, just return a tuple like (stdout, stderr) or (stdout, stderr, return code).

Now that we have our alias configured in xonsh, it’s time to add the functionality we want: taming mut.py’s output.

def _filter_mutpy(args, stdin=None):
    if stdin is None:
        return "No input to filter"

    output = []
    mutant = []
    collect_mutant = False
    for line in stdin:
        if " s] " in line and collect_mutant:
            collect_mutant = False
            mutant.append(line)
            if "incompetent" in line or "killed" in line:
                print(mutant[0], end="")
                print(mutant[-1], end="")
            else:
                print("".join(mutant), end="")
            mutant = []
        elif "- [#" in line and not collect_mutant:
            collect_mutant = True
            mutant.append(line)
        elif collect_mutant:
            mutant.append(line)
        else:
            print(line, end="")

aliases["filter_mutpy"] = _filter_mutpy

Now we can pipe mut.py into our alias and get this result:

$ mut.py --target example --unit-test test_example --runner pytest -m | filter_mutpy                                                                        
[*] Start mutation process:
   - targets: example
   - tests: test_example
[*] 2 tests passed:
   - test_example [0.52779 s]
[*] Start mutants generation and execution:
   - [#   1] AOR example: 
[0.12564 s] killed by test_example.py::test_not_prime
   - [#   2] AOR example: 
[0.12044 s] killed by test_example.py::test_not_prime
   - [#   3] BCR example: 
[0.12248 s] killed by test_example.py::test_not_prime
   - [#   4] COI example: 
[0.12042 s] killed by test_example.py::test_prime
   - [#   5] COI example: 
[0.11927 s] killed by test_example.py::test_prime
   - [#   6] CRP example: 
--------------------------------------------------------------------------------
   1: import math
   2: 
   3: def check_prime(number):
-  4:     if number < 2:
+  4:     if number < 3:
   5:         return False
   6:     for i in range(2, int(math.sqrt(number)) + 1):
   7:         if number % i == 0:
   8:             break
--------------------------------------------------------------------------------
[0.06259 s] survived
   - [#   7] CRP example: 
[0.12793 s] killed by test_example.py::test_not_prime
   - [#   8] CRP example: 
--------------------------------------------------------------------------------
   2: 
   3: def check_prime(number):
   4:     if number < 2:
   5:         return False
-  6:     for i in range(2, int(math.sqrt(number)) + 1):
+  6:     for i in range(2, int(math.sqrt(number)) + 2):
   7:         if number % i == 0:
   8:             break
   9:     else:
  10:         return True
--------------------------------------------------------------------------------
[0.06272 s] survived
   - [#   9] CRP example: 
[0.12543 s] killed by test_example.py::test_prime
   - [#  10] ROR example: 
[0.12325 s] killed by test_example.py::test_prime
   - [#  11] ROR example: 
--------------------------------------------------------------------------------
   1: import math
   2: 
   3: def check_prime(number):
-  4:     if number < 2:
+  4:     if number <= 2:
   5:         return False
   6:     for i in range(2, int(math.sqrt(number)) + 1):
   7:         if number % i == 0:
   8:             break
--------------------------------------------------------------------------------
[0.06679 s] survived
   - [#  12] ROR example: 
[0.12549 s] killed by test_example.py::test_prime
[*] Mutation score [2.04439 s]: 75.0%
   - all: 12
   - killed: 9 (75.0%)
   - survived: 3 (25.0%)
   - incompetent: 0 (0.0%)
   - timeout: 0 (0.0%)

Awesome! Every code snippet is now related to a mutant that survived, so we can see at a glance which ones are important—and I used that to improve the tests, so more cases are covered and more mutants are killed.

This is a relatively simple example of xonsh’s power, but remember that the entire Python standard library and ecosystem is available to parse, filter, and act on the output of any command-line interface.

I’m looking forward to discovering more ways to use callable aliases in my work. Got any ideas?

CategoriesPython

Generating a Starry Sky

Years ago, I discovered netpbm’s ppmforge, one option of which is to generate a starry sky, which fascinated me. The author’s website is full of interesting projects and papers, too. t I set out to rewrite it in Python, which I had just recently started learning at the time. Following is some code from 2012. If you want to compare it to ppmforge, the relevant lines are 63, 440-508. You’ll probably find that my younger self didn’t do a great job at translating everything to Python.

import random
import Image
import cStringIO
 
def cast(low, high):
    arand = (2.0**15.0) - 1.0
    return ((low)+(((high)-(low)) * ((random.randint(0, 12345678) & 0x7FFF) / arand)))
 
def make_star_image(width=600, height=600, star_fraction=3, star_quality=0.5, star_intensity=8, star_tint_exp=0.5, bg=None, lambd=0.0025):
    star_data = []
     
    if bg == None:
        star_image = Image.new("RGB", (width, height))
        for i in range(0, width):
            for l in range(0, height):
                if random.expovariate(1.5) < star_fraction:
                    v = int(star_intensity * ((1 / (1 - cast(0, 0.999))**star_quality)))
                    if v > 255:
                        v = 255
                    star_data.append((v, v, v))
                else:
                    star_data.append((0, 0, 0))
    else:
        index = 0
        if bg.mode != "RGB":
            bg = bg.convert("RGB")
        width, height = bg.size
        star_image = Image.new("RGB", (width, height))
        bg = bg.getdata()
        for i in range(0, width):
            for l in range(0, height):
                r, g, b = bg[index]
                average = (r + b + g) / 3
                r = random.expovariate(lambd)
                if r < average or random.random() > 0.9:
                    v = int(star_intensity * ((1 / (1 - cast(0, 0.999))**star_quality)))
                    if v > 255:
                        v = 255
                    if r < average:
                        if v > 40:
                            v = int(v * 1.5)
                            if v > 255:
                                v = 255
                            star_data.append((v, v, v))
                        elif v < 40 and random.random() > 0.5:
                            star_data.append((v, v, v))
                        else:
                            star_data.append((0, 0, 0))
                    else:
                        star_data.append((v, v, v))
                else:
                    star_data.append((0, 0, 0))
                index += 1
    star_image.putdata(star_data)
    return star_image
 
def main():
    make_star_image(width=1280, height=800, star_quality=1.2, lambd=0.0025, star_intensity=1, star_fraction=1).show()
    #bg = Image.open("/home/harrison/Pictures/wallpapers/bp1.jpg")
    #make_star_image(bg=bg, lambd=0.0035).show()
#    io = cStringIO.cStringIO()
#    starry.save(io, "JPEG")
#    data = io.getvalue()
#    
#    print "content-type: image/jpeg"
#    print
#    print data
 
if __name__ == "__main__":
    main()

The temperature calculation is off, the cast function isn’t quite right, and I didn’t even attempt the blackbody radiation calculations from the planck macro. The list could go on.

My most recent iteration of a starry sky generator is available on Github. This version is based on a deeper understanding of the math involved. For example, it turned out that the cast function was unnecessary, as its functionality is basically already built in to Python’s random module. I’m still working on understanding the planck function, so if you know much about blackbody radiation, I’d be happy to talk to you about it!

I like to compare these two versions of basically the same program, because it illustrates, in my mind, the idea of a Pythonic program. The improvements in the latest version are a result of better understanding both the problem and the solution.

The first version required the cast function because it just copied syntax over from the C program and made it work in Python. After taking the time to understand the problem that function was trying to solve, while also learning the best way to solve that problem in Python, it was able to be replaced entirely.

So, what would have been a troublesome function for a reader of the code to puzzle over, was turned into an easily-understood standard library function call.

Another example is in the control flow. The old version is deeply nested and confusing. The new one breaks more of it out into separate functions, uses clearer variable names, and makes better use of white space for grouping. I think it’s a lot easier to follow, besides looking prettier.

This comes up a lot when I’m solving code challenges on PyBites, too. When you compare solutions, it’s easy to see that some are better than others. Sometimes I’m happy with my solution, but sometimes it leaves a lot to be desired. It depends on how much experience I have with that kind of problem and how much time I put into understanding it.

The way to grow is to keep reading good examples of code and practicing. We can’t master every area, but we can keep improving!

There are several performance-related improvements in the new version, as well. For example, this version uses a bytearray to store the pixels before converting to an Image. In another post, I’ll go through the performance measurements I used to determine that this method is significantly faster than the other options.

I’m sure that there is still a lot that could be better about my latest starry sky generator, but it’s nice to be able to compare and see how much I’ve grown so far.

The Django app is currently hosted on Google AppEngine, so go ahead and check it out!

Or, see this page if you want to use text like the featured image.

CategoriesPython

Introduction to Xonsh

Recently, I got started with xonsh (pronounced “conch”) as a replacement shell for Bash.

What is xonsh, you might ask? Well, basically, it’s a version of Python meant for use as a shell. Since it’s a superset of Python, all Python programs are valid xonsh shell scripts, so you can make use of Python’s standard library and any other Python package you have available.

Probably my favorite feature, though, is being able to transfer my Python knowledge to shell scripting. As the feature comparison puts it, xonsh is a “sane language.”

That means we can do math (and a lot more!) directly in the shell, like so:

$ (5 + 5) ** 5
100000

However, we can also write commands, just like in Bash:

$ curl example.com
<!doctype html>
<html>
<head>
    <title>Example Domain</title>
...

Xonsh handles this by having two modes, which it automatically chooses between for each line. Python mode is the default, but any time a line contains only an expression statement, and the names are not all current variables, it will be interpreted as a command in subprocess mode.

When you install xonsh and run it without a .xonshrc file, you’ll be presented with a welcome screen:

            Welcome to the xonsh shell (0.9.13.dev1)                              

            ~ The only shell that is also a shell ~                              

----------------------------------------------------
xonfig tutorial   ->    Launch the tutorial in the browser
xonfig wizard     ->    Run the configuration wizard and claim your shell 
(Note: Run the Wizard or create a ~/.xonshrc file to suppress the welcome screen)

Going through the wizard will present you with a ton of options. Most will not be necessary to mess with, but there are some useful things like changing the prompt, and various plugins.

That’s just the tip of the iceberg. Xonsh has a lot more to offer, but I’m still exploring the possibilities.

For further reading, the guides cover a lot of topics in-depth.