You've got a temperature reading. In real terms, one decimal. Readable. Day to day, you need to show it to a human — or log it, or send it somewhere — as 23. The raw value sits there: 23.Clean. Because of that, 456789. 5 C. Maybe it's from a sensor, an API, or a calculation. The unit right there It's one of those things that adds up..
Sounds trivial. Until it isn't.
What This Formatting Actually Means
Printing air_temperature with one decimal point followed by C isn't just about aesthetics. It's about contract — between your code and whatever consumes the output. Because of that, a dashboard. Because of that, a log parser. A human operator at 3 AM reading an alert.
The requirement breaks down to three things:
- Round (or truncate) to one decimal place
- Append the degree Celsius indicator
- Do it consistently, every time
air_temperature is just a variable name. Could be temp_c, current_temp, reading. The name doesn't matter. The format does.
Where This Shows Up
Embedded firmware printing to serial. Python scripts writing CSV. MicroPython on a Pico. That said, data loggers. CLI tools. Home Assistant templates. Arduino sketches. Anywhere temperature meets text The details matter here..
And every environment handles it differently Not complicated — just consistent..
Why the Details Matter
You've seen the bad versions:
23.5 °C(Unicode degree symbol — breaks some log shippers)23.5 c(lowercase — not standard)23.5C(no space, hard to parse)23.50 C(two decimals — false precision)- `23.
Consistency isn't pedantry. It's operational hygiene.
A log line like 2024-01-15T03:22:11Z sensor=outdoor temp=23.This leads to 5 C gets parsed by regex, ingested by Loki, graphed in Grafana. One space missing? The field extraction fails. Here's the thing — the alert doesn't fire. You wake up at 3 AM anyway — but now you're debugging formatting.
How to Do It in the Real World
Python (Standard Library)
air_temperature = 23.456789
print(f"{air_temperature:.1f} C")
Output: 23.5 C
The :.Know that. 23.In real terms, rounding is banker's rounding (round half to even). Practically speaking, 23. Plus, 4. On top of that, 6. 45 becomes 23.1f does the heavy lifting — fixed-point, one decimal. 55becomes23.Document it if it matters.
Don't use round(air_temperature, 1) then string-concatenate. Even so, that's two steps. So two places to typo. f-strings are atomic That's the whole idea..
Python (Logging Module)
import logging
logging.basicConfig(format='%(message)s')
logger = logging.getLogger(__name__)
air_temperature = 23.456789
logger.info(f"{air_temperature:.1f} C")
Same formatting. But now you get timestamps, levels, structured output — for free.
MicroPython / CircuitPython
air_temperature = 23.456789
print("{:.1f} C".format(air_temperature))
No f-strings on older MicroPython builds. Here's the thing — . format() works everywhere. Same rounding behavior.
Arduino / C++
float air_temperature = 23.456789;
Serial.print(air_temperature, 1);
Serial.println(" C");
Serial.print(val, 1) prints one decimal. But — it doesn't round the same way. On the flip side, it rounds half away from zero. 23.45 → 23.5. Still, different from Python. If you're correlating logs across systems, this bites.
Better: use dtostrf for full control.
char buf[16];
dtostrf(air_temperature, 5, 1, buf); // width 5, 1 decimal
Serial.print(buf);
Serial.println(" C");
Bash / Shell
air_temperature=23.456789
printf "%.1f C\n" "$air_temperature"
printf is POSIX. Works in sh, bash, zsh. No external deps. Rounding is half-away-from-zero (like C) Small thing, real impact..
JavaScript / Node.js
const airTemperature = 23.456789;
console.log(`${airTemperature.toFixed(1)} C`);
toFixed(1) returns a string. Consistent with C/Arduino. Because of that, rounding is half-away-from-zero. Not consistent with Python And that's really what it comes down to..
Go
airTemperature := 23.456789
fmt.Printf("%.1f C\n", airTemperature)
Go uses half-away-from-zero too. The C family sticks together.
Rust
let air_temperature = 23.456789_f32;
println!("{:.1} C", air_temperature);
Rust defaults to half-away-from-zero. But you can choose: round::half_up, half_even, etc. Explicit is good Most people skip this — try not to..
SQL (PostgreSQL)
SELECT round(air_temperature::numeric, 1) || ' C' AS formatted_temp
FROM sensor_readings;
Database-side formatting. Useful when the DB is the source of truth for dashboards.
InfluxDB Line Protocol
sensor,location=outdoor air_temperature=23.5
No unit in the value. Still, the unit lives in the measurement name or tag. Telegraf, Grafana, and friends expect this. Don't stuff C into the field value.
Common Mistakes (And Why They Happen)
Mistake 1: String Concatenation With round()
# Don't
print(str(round(air_temperature, 1)) + " C")
Why it's bad: round() returns a float. And str(23. Day to day, 5) → "23. On top of that, 5". Worth adding: fine. But str(23.0) → "23.Because of that, 0" — wait, actually it's "23. So 0" in Python 3. But round(23.05, 1) → 23.Day to day, 1 (banker's rounding). Then str() → "23.1". Also, works. But it's two operations. Two mental models. f-string is one.
Easier said than done, but still worth knowing.
Mistake 2: Hardcoding the Degree Symbol
print(f"{air_temperature:.1f} °C") # Unicode U+00B0
Looks pretty in a terminal. Breaks in:
- Old log shippers (Filebeat, Logstash with default codecs)
- CSV parsers expecting ASCII
- Embedded displays with limited font tables
greppatterns written forC
Stick to C. Or degC if you need unamb
Mistake 3: Ignoring Locale‑Dependent Decimal Separators
# On a French system, the comma is the decimal separator
print(f"{air_temperature:.1f} C") # → 23,5 C in the console
If the log is later consumed by a system that expects a dot, the value is mis‑parsed.
Solution: Force the dot by using the locale‑independent formatting functions (format, f‑string, printf‑style) or set the locale explicitly before formatting Simple as that..
Mistake 4: Mixing “pretty” and “machine‑readable” formats in the same stream
[INFO] 2026‑08‑06 12:34:56 – Temperature: 23.5 C
[DEBUG] 2026‑08‑06 12:34:56 – Temp: 23.5
If you later parse the logs, you’ll have two different tokens for the same value. Keep a single, canonical representation in the log line and, if you need a human‑friendly string, generate it at display time, not at write time Simple as that..
Mistake 5: Over‑formatting to a Fixed Width
"023.5 C" # padded with zeros
Padding is useful for human‑readable tables, but it introduces unnecessary characters when the value is parsed by a machine. Stick to the shortest representation that preserves the required precision Simple, but easy to overlook..
Best‑Practice Checklist
| ✅ | Recommendation | Why |
|---|---|---|
| 1 | Use a single, language‑native formatting function (e. | Guarantees consistent rounding and locale‑independence. In practice, |
| 8 | Avoid string concatenation for numeric values. , pint in Python, units in Rust). Think about it: |
|
| 2 | Always round after the value has been computed, not before. Plus, | |
| 4 | Prefer ASCII over Unicode for log files that may be processed by legacy tools. | Avoids mis‑interpretation of the degree symbol or other glyphs. |
| 7 | If you need to convert units, do it once before formatting. In practice, | |
| 3 | Keep the unit out of the numeric value. ). Which means | Avoids truncation errors that can accumulate in a pipeline. That said, |
| 10 | Keep logs machine‑friendly: no trailing spaces, no extra symbols, and a consistent delimiter (CSV, JSON, line‑protocol). Day to day, | Keeps the log value in a single canonical unit (e. And |
| 9 | Use a dedicated library for units (e. That's why printf‑style in C/Go, format in Python, toFixed in JavaScript). Also, g. g.In practice, , Celsius). |
|
| 5 | Document the chosen rounding mode (half‑even, half‑up, etc.g.Here's the thing — | Reduces bugs where the numeric part is accidentally converted to a string with a different format. Plus, |
| 6 | Validate the output with unit tests that compare against a reference implementation. | Simplifies downstream ingestion and querying. |
A Practical Example: End‑to‑End Pipeline
-
Sensor firmware (C/Arduino):
dtostrf(air_temperature, 5, 1, buf); // 23.5 Serial.println(buf); // raw value, no unit -
Telegraf (Line Protocol):
sensor,location=outdoor air_temperature=23.5 -
InfluxDB stores
air_temperatureas afloatThat's the whole idea.. -
Grafana visualises the raw numeric value and adds the unit in the panel title:
“Outdoor Air Temperature (°C)” Surprisingly effective.. -
Python ETL reads the series, rounds to the desired precision, and writes a CSV for a reporting tool:
df['air_temperature'] = df['air_temperature'].round(1) df.to_csv('report.csv', index=False, float_format='%.1f')
The same value travels unchanged through the pipeline; only the presentation layer adds the unit or the degree symbol.
Conclusion
Formatting a temperature value once and for all may seem trivial, but the devil is in the details. Different languages round differently, different
Beyond the Basics – Pitfalls You’ll Encounter in the Real World
| Situation | What can go wrong | How to keep it clean |
|---|---|---|
| Negative temperatures | Mixing a “‑” sign with a degree symbol (e.On top of that, | Clamp the raw reading at the firmware level and document the safe range in the schema. In practice, 2e`) that matches the required precision. 5°”) can break parsers that expect the unit after the number. , “‑23. |
| Locale‑specific decimal separators | Writing 23,5 for a European audience will be interpreted as 235 by a CSV parser expecting a dot. , unit=celsius) duplicates information that should be derived from the column name. Here's the thing — g. And |
|
| Dependency on a third‑party library that changes rounding behavior | A library upgrade could silently switch from “half‑even” to “half‑up”, breaking historic comparisons. | Apply a single rounding operation at the point where the value is formatted for human consumption. Plus, 3f, %. Because of that, |
| Multiple rounding passes | Rounding a rounded value again can drift the series away from the original measurement. | Keep tags for dimensions (location, sensor_id) and let the measurement name or column label carry the unit. |
| Very large or very small numbers | Floating‑point underflow/overflow may silently produce inf or 0. In practice, 0 when the sensor range is exceeded. And |
Store the raw signed value; let the UI prepend the sign and the degree glyph. |
| Mixed line‑protocol styles | Adding extra tags (e. | Stick to ASCII dot (`. |
| Unit conversion in the middle of a pipeline | Converting Celsius to Fahrenheit before persisting forces you to keep two numeric columns or risk losing the canonical unit. Here's the thing — | |
| Loss of precision when serialising | Using str() or printf("%g") may drop trailing zeros or switch to scientific notation unexpectedly. In practice, |
Convert once, right before the presentation layer, and keep the original column in the canonical unit. |
A Quick Reference Checklist
- [ ] Raw value stored as a plain number (no unit, no degree sign).
- [ ] Canonical unit defined in the schema or column comment.
- [ ] Single rounding step applied only when formatting for display or export.
- [ ] Consistent decimal separator (
.) and ASCII characters in logs. - [ ] Library pinning and a test suite that validates rounding against a known reference.
- [ ] Unit conversion performed once, just before the presentation layer.
- [ ] Edge‑case handling (clamp, NaN/inf detection) at the source.
- [ ] Documentation of rounding mode, precision, and safe measurement range.
Putting It All Together – A Minimal End‑to‑End Example
Below is a compact, language‑agnostic flow that respects every rule above.
-
Firmware (C/Arduino) – emit the raw temperature as a plain ASCII string with a dot separator The details matter here..
// air_temperature_celsius holds the unrounded sensor reading char buf[16]; dtostrf(air_temperature_celsius, 6, 3, buf); // e.g. "23.456" Serial.println(buf); -
Ingestion (Telegraf → Line Protocol) – keep the field name as the measurement and let the tag describe the location.
sensor,location=outside air_temperature_celsius=23.456 -
Storage (InfluxDB) –
air_temperature_celsiusis stored as afloat. No extra column for units No workaround needed.. -
Visualization (Grafana) –
Visualization (Grafana) – apply formatting in the panel options, not in the query.
-- Flux/InfluxQL query returns the raw canonical value
from(bucket: "telem")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "sensor" and r._field == "air_temperature_celsius")
In the Value mappings or Standard options pane, set Unit → Temperature → Celsius (°C) and Decimals → 1. Grafana will render 23.5 °C while the underlying datum remains the untouched 23.456.
- Export / Reporting (Python) – convert once, at the very edge of the system.
import pandas as pd df = pd.read_parquet("sensor_export.That's why parquet") # column: air_temperature_celsius (float64) df["air_temperature_fahrenheit"] = df["air_temperature_celsius"] * 9/5 + 32 df["air_temperature_fahrenheit"] = df["air_temperature_fahrenheit"]. round(1) # single rounding step df.to_csv("report.csv", float_format="%.
Conclusion
Treating numeric telemetry as data first, presentation second eliminates an entire class of subtle bugs: drifting precision, duplicated unit columns, locale-dependent parsing errors, and silent rounding regressions. Here's the thing — by storing a single canonical value, deferring every format decision to the presentation layer, and locking down library behavior with pinned dependencies and regression tests, you gain reproducibility across firmware, ingestion pipelines, time-series databases, dashboards, and exported reports. The discipline is minimal—one rounding step, one conversion, one source of truth—but the payoff is a system that remains auditable and stable as requirements, libraries, and visualization tools evolve Took long enough..