## Converting Celsius to Fahrenheit round-up

**July 22, 2024**

0 comments Go, 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.

- Converting Celsius to Fahrenheit with Python
- Converting Celsius to Fahrenheit with TypeScript
- Converting Celsius to Fahrenheit with Go
- Converting Celsius to Fahrenheit with Ruby
- Converting Celsius to Fahrenheit with Crystal
- Converting Celsius to Fahrenheit with Rust

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, 2024**

0 comments Python

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, 2024**

1 comment Python

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, 2024**

0 comments Python, 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, 2024**

1 comment Django, Python

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

- The first N results
- 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, 2023**

0 comments Python

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`