## Converting Celsius to Fahrenheit round-up

### July 22, 20240 commentsGo, Node, Python, Bun, Ruby, Rust, JavaScript

In the last couple of days, I've created variations of a simple algorithm to demonstrate how Celcius and Fahrenheit seem to relate to each other if you "mirror the number".
It wasn't supposed to be about the programming language. Still, I used Python in the first one and I noticed that since the code is simple, it could be fun to write variants of it in other languages.

It was a fun exercise.

And speaking of fun, I couldn't help but to throw in a benchmark using `hyperfine` that measures, essentially, how fast these CLIs can start up. The results look like this:

``````
Summary
./conversion-rs ran
1.31 ± 1.30 times faster than ./conversion-go
1.88 ± 1.33 times faster than ./conversion-cr
7.15 ± 4.64 times faster than bun run conversion.ts
14.27 ± 9.48 times faster than python3.12 conversion.py
18.10 ± 12.35 times faster than node conversion.js
67.75 ± 43.80 times faster than ruby conversion.rb
``````

It doesn't prove much, that you didn't expect. But it's fun to see how fast Python 3.12 has become at starting up.

Head on over to https://github.com/peterbe/temperature-conversion to play along. Perhaps you can see some easy optimizations (speed and style).

## Converting Celsius to Fahrenheit with Python

### July 12, 20240 commentsPython

Here's a useful mnemonic for remembering how to convert Celsius to Fahrenhait(*):

• Start at 4°C
• Add +12 each time
• Flip the C in mirror, with some additional fudging

For example, 4°C is 04°C. Mirror image of "04" is "40". So 4°C equals 40°F.
And when there's a 1 in front, as in 125°F, look at that as 100 + 25°F. Mirror of 25°F is 52°C. So 52°C equals 125°F.

In Python it can be tested like this:

``````
import math

def c2f(c):
return c * 9 / 5 + 32

def is_mirror(a, b):
def massage(n):
if n < 10:
return f"0{n}"
elif n >= 100:
return massage(n - 100)
else:
return str(n)

return massage(a)[::-1] == massage(b)

def print_conv(c, f):
print(f"{c}°C ~= {f}°F")

for i in range(4, 100, 12):
f = c2f(i)
if is_mirror(i, math.ceil(f)):
print_conv(i, math.ceil(f))
elif is_mirror(i, math.floor(f)):
print_conv(i, math.floor(f))
else:
break
``````

When you run that you get:

```4°C ~= 40°F
16°C ~= 61°F
28°C ~= 82°F
40°C ~= 104°F
52°C ~= 125°F```

(*) If you can't remember F = C × 9/5 + 32 or, perhaps, remember it but can't compute the arithmetic easily.

## How do you thousands-comma AND whitespace format a f-string in Python

### March 17, 20241 commentPython

For some reason, I always forget how to do this. Tired of that. Let's blog about it so it sticks.

To format a number with thousand-commas you do:

``````
>>> n = 1234567
>>> f"{n:,}"
'1,234,567'
``````

To add whitespace to a string you do:

``````
>>> name="peter"
>>> f"{name:<20}"
'peter               '
``````

How to combine these in one expression, you do:

``````
>>> n = 1234567
>>> f"{n:<15,}"
'1,234,567      '
``````

## Leibniz formula for π in Python, JavaScript, and Ruby

### March 14, 20240 commentsPython, JavaScript

Officially, I'm one day behind, but here's how you can calculate the value of π using the Leibniz formula.

### Python

``````
import math

sum = 0
estimate = 0
i = 0
epsilon = 0.0001
while abs(estimate - math.pi) > epsilon:
sum += (-1) ** i / (2 * i + 1)
estimate = sum * 4
i += 1
print(
f"After {i} iterations, the estimate is {estimate} and the real pi is {math.pi} "
f"(difference of {abs(estimate - math.pi)})"
)
``````

Outputs:

`After 10000 iterations, the estimate is 3.1414926535900345 and the real pi is 3.141592653589793 (difference of 9.99999997586265e-05)`

### JavaScript

``````
let sum = 0;
let estimate = 0;
let i = 0;
const epsilon = 0.0001;

while (Math.abs(estimate - Math.PI) > epsilon) {
sum += (-1) ** i / (2 * i + 1);
estimate = sum * 4;
i += 1;
}
console.log(
`After \${i} iterations, the estimate is \${estimate} and the real pi is \${Math.PI} ` +
`(difference of \${Math.abs(estimate - Math.PI)})`
);
``````

Outputs

`After 10000 iterations, the estimate is 3.1414926535900345 and the real pi is 3.141592653589793 (difference of 0.0000999999997586265)`

### Ruby

``````
sum = 0
estimate = 0
i = 0
epsilon = 0.0001
while (estimate - Math::PI).abs > epsilon
sum += ((-1) ** i / (2.0 * i + 1))
estimate = sum * 4
i += 1
end
print(
"After #{i} iterations, the estimate is #{estimate} and the real pi is #{Math::PI} "+
"(difference of #{(estimate - Math::PI).abs})"
)
``````

Outputs

`After 10000 iterations, the estimate is 3.1414926535900345 and the real pi is 3.141592653589793 (difference of 9.99999997586265e-05)`

## Backwards

Technically, these little snippets are checking that it works since each language already has access to a value of π as a standard library constant.

If you don't have that, you can decide on a number of iterations, for example 1,000, and use that.

### Python

``````
sum = 0
for i in range(1000):
sum += (-1) ** i / (2 * i + 1)
print(sum * 4)
``````

### JavaScript

``````
let sum = 0;
for (const i of [...Array(10000).keys()]) {
sum += (-1) ** i / (2 * i + 1);
}
console.log(sum * 4);
``````

### Ruby

``````
sum = 0
for i in 0..10000
sum += ((-1) ** i / (2.0 * i + 1))
end
puts sum * 4
``````

## Performance test

Perhaps a bit silly but also a fun thing to play with. Pull out `hyperfine` and compare Python 3.12, Node 20.11, Ruby 3.2, and Bun 1.0.30:

``````
❯ hyperfine --warmup 10 "python3.12 ~/pi.py" "node ~/pi.js" "ruby ~/pi.rb" "bun run ~/pi.js"
Benchmark 1: python3.12 ~/pi.py
Time (mean ± σ):      53.4 ms ±   7.5 ms    [User: 31.9 ms, System: 12.3 ms]
Range (min … max):    41.5 ms …  64.8 ms    44 runs

Benchmark 2: node ~/pi.js
Time (mean ± σ):      57.5 ms ±  10.6 ms    [User: 43.3 ms, System: 11.0 ms]
Range (min … max):    46.2 ms …  82.6 ms    35 runs

Benchmark 3: ruby ~/pi.rb
Time (mean ± σ):     242.1 ms ±  11.6 ms    [User: 68.4 ms, System: 37.2 ms]
Range (min … max):   227.3 ms … 265.3 ms    11 runs

Benchmark 4: bun run ~/pi.js
Time (mean ± σ):      32.9 ms ±   6.3 ms    [User: 14.1 ms, System: 10.0 ms]
Range (min … max):    17.1 ms …  41.9 ms    60 runs

Summary
bun run ~/pi.js ran
1.62 ± 0.39 times faster than python3.12 ~/pi.py
1.75 ± 0.46 times faster than node ~/pi.js
7.35 ± 1.45 times faster than ruby ~/pi.rb
``````

### Comparing Pythons

Just because I have a couple of these installed:

``````
❯ hyperfine --warmup 10 "python3.8 ~/pi.py" "python3.9 ~/pi.py" "python3.10 ~/pi.py" "python3.11 ~/pi.py" "python3.12 ~/pi.py"
Benchmark 1: python3.8 ~/pi.py
Time (mean ± σ):      54.6 ms ±   8.1 ms    [User: 33.0 ms, System: 11.4 ms]
Range (min … max):    40.0 ms …  69.7 ms    56 runs

Benchmark 2: python3.9 ~/pi.py
Time (mean ± σ):      54.9 ms ±   8.0 ms    [User: 32.2 ms, System: 12.3 ms]
Range (min … max):    42.3 ms …  70.1 ms    38 runs

Benchmark 3: python3.10 ~/pi.py
Time (mean ± σ):      54.7 ms ±   7.5 ms    [User: 33.0 ms, System: 11.8 ms]
Range (min … max):    42.3 ms …  78.1 ms    44 runs

Benchmark 4: python3.11 ~/pi.py
Time (mean ± σ):      53.8 ms ±   6.0 ms    [User: 32.7 ms, System: 13.0 ms]
Range (min … max):    44.8 ms …  70.3 ms    42 runs

Benchmark 5: python3.12 ~/pi.py
Time (mean ± σ):      53.0 ms ±   6.4 ms    [User: 31.8 ms, System: 12.3 ms]
Range (min … max):    43.8 ms …  63.5 ms    42 runs

Summary
python3.12 ~/pi.py ran
1.02 ± 0.17 times faster than python3.11 ~/pi.py
1.03 ± 0.20 times faster than python3.8 ~/pi.py
1.03 ± 0.19 times faster than python3.10 ~/pi.py
1.04 ± 0.20 times faster than python3.9 ~/pi.py
``````

## How to avoid a count query in Django if you can

### February 14, 20241 commentDjango, Python

Suppose you have a complex Django QuerySet query that is somewhat costly (in other words slow). And suppose you want to return:

1. The first N results
2. A count of the total possible results

So your implementation might be something like this:

``````
def get_results(queryset, fields, size):
count = queryset.count()
results = []
for record in queryset.values(*fields)[:size]
results.append(record)
return {"count": count, "results": results}
``````

That'll work. If there are 1,234 rows in your database table that match those specific filters, what you might get back from this is:

``````
>>> results = get_results(my_queryset, ("name", "age"), 5)
>>> results["count"]
1234
>>> len(results["results"])
5
``````

Or, if the filters would only match 3 rows in your database table:

``````
>>> results = get_results(my_queryset, ("name", "age"), 5)
>>> results["count"]
3
>>> len(results["results"])
3
``````

Between your Python application and your database you'll see:

```query 1: SELECT COUNT(*) FROM my_database WHERE ...
query 2: SELECT name, age FROM my_database WHERE ... LIMIT 5```

The problem with this is that, in the latter case, you had to send two database queries when all you needed was one.
If you knew it would only match a tiny amount of records, you could do this:

``````
def get_results(queryset, fields, size):
-   count = queryset.count()
results = []
for record in queryset.values(*fields)[:size]:
results.append(record)
+   count = len(results)
return {"count": count, "results": results}
``````

But that is wrong. The `count` would max out at whatever the `size` is.

The solution is to try to avoid the potentially unnecessary `.count()` query.

``````
def get_results(queryset, fields, size):
count = 0
results = []
for i, record in enumerate(queryset.values(*fields)[: size + 1]):
if i == size:
# Alas, there are more records than the pagination
count = queryset.count()
break
count = i + 1
results.append(record)
return {"count": count, "results": results}
``````

This way, you only incur one database query when there wasn't that much to find, but if there was more than what the pagination called for, you have to incur that extra database query.

## Pip-Outdated.py with interactive upgrade

### September 21, 20230 commentsPython

Last year I wrote a nifty script called `Pip-Outdated.py` "Pip-Outdated.py - a script to compare requirements.in with the output of pip list --outdated". It basically runs `pip list --outdated` but filters based on the packages mentioned in your `requirements.in`. For people familiar with Node, it's like checking all installed packages in `node_modules` if they have upgrades, but filter it down by only those mentioned in your `package.json`.

I use this script often enough that I added a little interactive input to ask if it should edit `requirements.in` for you for each possible upgrade. Looks like this:

``````
❯ Pip-Outdated.py
black               INSTALLED: 23.7.0    POSSIBLE: 23.9.1
click               INSTALLED: 8.1.6     POSSIBLE: 8.1.7
elasticsearch-dsl   INSTALLED: 7.4.1     POSSIBLE: 8.9.0
fastapi             INSTALLED: 0.101.0   POSSIBLE: 0.103.1
httpx               INSTALLED: 0.24.1    POSSIBLE: 0.25.0
pytest              INSTALLED: 7.4.0     POSSIBLE: 7.4.2

Update black from 23.7.0 to 23.9.1? [y/N/q] y
Update click from 8.1.6 to 8.1.7? [y/N/q] y
Update elasticsearch-dsl from 7.4.1 to 8.9.0? [y/N/q] n
Update fastapi from 0.101.0 to 0.103.1? [y/N/q] n
Update httpx from 0.24.1 to 0.25.0? [y/N/q] n
Update pytest from 7.4.0 to 7.4.2? [y/N/q] y
``````

and then,

``````
❯ git diff requirements.in | cat
diff --git a/requirements.in b/requirements.in
index b7a246e..0e996e5 100644
--- a/requirements.in
+++ b/requirements.in
@@ -9,7 +9,7 @@ python-decouple==3.8
fastapi==0.101.0
uvicorn[standard]==0.23.2
selectolax==0.3.16
-click==8.1.6
+click==8.1.7
python-dateutil==2.8.2
gunicorn==21.2.0
# I don't think this needs `[secure]` because it's only used by
@@ -18,7 +18,7 @@ requests==2.31.0
cachetools==5.3.1

# Dev things
-black==23.7.0
+black==23.9.1
flake8==6.1.0
-pytest==7.4.0
+pytest==7.4.2
httpx==0.24.1
``````

That's it. Then if you want to actually make these upgrades you run:

``````
❯ pip-compile --generate-hashes requirements.in && pip install -r requirements.txt
``````

To install it, download the script from: https://gist.github.com/peterbe/a2b158c39f1f835c0977c82befd94cdf
and put it in your `~/bin` and make it executable.
Now go into a directory that has a `requirements.in` and run `Pip-Outdated.py`

Previous page
Next page