18 Choosing a Programming Language
More
Depth: Practitioner
Progress saves as you read. Mark sections complete as you go; your progress appears in the sidebar.
A greenhouse team explores a watering rule on a laptop. The rule works, but the final board has less memory, wakes from sleep, and reads real pins. The laptop result answers a logic question. It leaves the board’s timing and energy questions open.
A language runs through a particular runtime and toolchain on a particular target. Choose that combination according to the behaviour you need to observe now. A short script can make calibration easy to inspect; a target build can reveal driver, memory, and wake-up limits.
For a worked example, suppose the prototype rule requests watering below 30% moisture. Keep shared examples: 29% requests watering, 30% does not, and a missing reading produces an input fault. Run those examples in the host script and the firmware implementation. Matching outputs support transfer of the rule. Then measure boot-to-sample time, memory headroom, and sleep current on the board; matching logic says nothing about those measurements.
The common misconception is that a successful script proves the intended device can run it within its limits. Keep the rule, units, and test vectors as reusable evidence. Choose and measure the target runtime separately, recording which parts transfer and which need rebuilding.
18.1 Start With the Story
This section: Start With the Story — The language question is rarely “C, C++, Python, JavaScript, or Rust?” in isolation.
The language question is rarely “C, C++, Python, JavaScript, or Rust?” in isolation. A prototype language must fit the evidence surface: interrupt timing, heap growth, sensor drivers, gateway scripts, cloud tests, UI workflows, update packaging, or team handoff. A language can make one risk easy while hiding another.
Read this chapter as a way to match code to proof. Choose the language that makes the current behavior measurable, record what the runtime adds or hides, and mark which later prototype must revisit the choice before production.
18.2 Choose Runtime by Risk
This section: Choose Runtime by Risk — In Figure 18.1, Language choice should follow the prototype risk.
In Figure 18.1, Language choice should follow the prototype risk. A host Python script can prove a calibration rule quickly, but it cannot prove MCU sleep current. C or C++ can expose vendor-driver and interrupt behavior, but they do not automatically produce maintainable state logic. Rust can reduce memory-safety risk, but only if the target support, build flow, and team review path are real.
In Figure 18.1, Most IoT prototypes use several languages at once. The firmware, gateway, dashboard, cloud function, data notebook, and test harness should each have a named evidence role and a boundary that says what transfers to the next form.
For example, a cold-room monitor team might use CPython and pandas to explore calibration curves from CSV exports, TypeScript to validate dashboard state and API contracts, and C++ with ESP-IDF or Arduino core to prove boot, I2C sensor timing, MQTT publish behavior, watchdog recovery, and deep-sleep current on an ESP32-C3. The high-level script can preserve threshold logic and test vectors, but it cannot prove heap headroom, radio association time, OTA slot size, or wake-to-publish energy on the board.
The same rule applies to gateway-heavy prototypes. Node-RED, Node.js, Go, or Rust can be a good fit for Linux protocol bridges, local SQLite queues, systemd services, and field-service scripts. Those choices prove gateway behavior, not microcontroller interrupt latency. A reliable language review states which runtime owns each evidence question and which shared artifacts cross the boundary: JSON Schema, Protocol Buffers definitions, CBOR maps, state names, unit conventions, golden payloads, and fault examples.
- Device question: which language proves timing, memory, power, driver access, watchdog, and update behavior on the target?
- Gateway question: which language proves protocol bridging, local storage, field scripts, and service diagnostics?
- Shared-interface question: which schemas, test vectors, payload examples, and state names stay stable across runtimes?
18.3 Match Language to Execution
This section: Match Language to Execution — Embedded slices may use C, C++, Rust, MicroPython, CircuitPython, Arduino, ESP-IDF, Zephyr, FreeRTOS, Embassy, RTIC, or vendor SDKs.
Embedded slices may use C, C++, Rust, MicroPython, CircuitPython, Arduino, ESP-IDF, Zephyr, FreeRTOS, Embassy, RTIC, or vendor SDKs. Gateway and service slices may use CPython, Node.js, TypeScript, Go, Rust, Node-RED, Docker Compose services, MQTT clients, SQLite, Redis, or local systemd units. Test and data slices may use pytest, Vitest, Jest, Robot Framework, Jupyter notebooks, pandas, JSON Schema, Protocol Buffers, OpenAPI, CBOR fixtures, and golden payload files.
Start with the execution surface. If the evidence depends on an STM32 ADC, nRF52840 BLE stack, ESP32 Wi-Fi reconnect, RP2040 PIO timing, or LoRa radio sleep state, run a target firmware slice and record compiler version, board profile, linker map, binary size, stack use, heap trend, reset reason, and current trace. If the evidence depends on broker policy, local queue replay, or service restart behavior, a Linux gateway runtime with Mosquitto, EMQX, SQLite, Docker Compose, or systemd may be the right place to prove it.
Then keep language-neutral assets explicit. Calibration formulas can be checked with host tests and then ported to firmware. Payload contracts can live in JSON Schema, .proto files, OpenAPI examples, or CBOR fixtures. State-machine examples can be shared as input/output vectors even when firmware uses C++ and the dashboard uses TypeScript. These artifacts reduce rewrite risk because reviewers can compare behavior across runtimes instead of trusting that a manual port stayed equivalent.
- For C/C++ firmware: measure binary size, stack depth, heap use, interrupt latency, watchdog behavior, boot time, sensor timing, and OTA slot size.
- For Rust firmware: verify HAL support, no-std crates, allocator policy, panic behavior, probe-rs or defmt logging, RTIC or Embassy task behavior, and team review readiness.
- For Python-family prototypes: separate calibration logic, payload shaping, and gateway automation from claims about MCU memory, sleep current, startup time, and deterministic control.
- For TypeScript or JavaScript: define the event-loop boundary, dependency lockfile, API contract, dashboard state model, gateway runtime, and offline behavior before treating it as device evidence.
Use a language review record before handoff. Include the runtime role, evidence it proves, evidence it does not prove, dependency lockfile, test command, target or host environment, shared interface files, and the condition that requires a board measurement. A quick MicroPython or CircuitPython demo can be valuable if the record says it proves sensor discovery and calibration shape, while final sleep current, watchdog behavior, flash layout, and OTA recovery still need the production firmware runtime.
18.4 Boundaries Carry Product Risk
This section: Boundaries Carry Product Risk — A language boundary is also a unit, timing, ownership, and failure boundary.
A language boundary is also a unit, timing, ownership, and failure boundary. Bugs often appear when a host script uses floating-point units differently than firmware, a gateway changes null handling, a dashboard assumes fresh state, or a firmware port changes payload field names.
Numeric and timing differences are common. Python may use floating point comfortably while firmware moves to fixed-point integer math. JavaScript may treat missing, null, and undefined values differently from a C struct decoder. A Rust no-std build may avoid heap allocation while a gateway implementation uses dynamic JSON parsing. An event loop on Node.js can hide blocking calls that would starve a microcontroller control loop. The shared tests should catch unit conversion, rounding, stale-state, and missing-field behavior before those differences reach a pilot.
Runtime costs also decide whether a language is evidence for the target. Garbage collection, interpreter startup, package size, TLS library footprint, task stack, allocator policy, exception handling, debug logging, and update image size can change memory, power, and recovery behavior. Record those costs with the same seriousness as source code. For a battery node, boot-to-sample time, wake-to-publish time, heap high-water mark, sleep current, and watchdog recovery matter more than how compact the prototype source looks.
- Schema boundary: keep JSON Schema,
.protofiles, OpenAPI examples, CBOR maps, unit conventions, timestamp rules, and error states versioned with test vectors. - Runtime boundary: record interpreter footprint, garbage collection, event-loop delays, thread/task model, blocking calls, package size, startup time, and update footprint.
- Tooling boundary: record compiler version, board profile, SDK version, package lockfile, linker script, feature flags, static-analysis output, and debug/probe method.
- Handoff boundary: state what to keep, rewrite, check on target, or discard when moving from script, simulator, gateway app, or dashboard code to target firmware.
The language decision is strong when a reviewer can repeat the target run, host run, and shared-interface tests and see which constraints each runtime actually proved.
A mixed-language prototype is therefore healthy only when it has a boundary owner. One owner keeps the schema and test vectors current. One owner confirms target measurements after firmware changes. One owner records dashboard or gateway assumptions. Without those owners, each language can be locally reasonable while the system contract drifts across payload fields, units, error states, and retry behavior.
18.5 Learning Objectives
This section: Learning Objectives
By the end of this chapter, you will be able to:
- Choose a prototype language from evidence needs rather than habit or tool preference.
- Separate device firmware, gateway, application, data, and test-harness language roles.
- Identify when target timing, memory, power, safety, or update constraints require target-specific measurement.
- Record what a language prototype proves, what it does not prove, and what must be checked again before production.
- Plan mixed-language handoff without letting a quick prototype become unreviewed production code.
18.6 Language Choice Is Evidence Choice
This section: Language Choice Is Evidence Choice — The development environment chapter recorded how a prototype is built, uploaded, and observed.
The development environment chapter recorded how a prototype is built, uploaded, and observed. This chapter asks what language should carry each software risk. The useful question is not “Which language is best?” but “Which language makes this evidence easiest to trust?”
Frame the language decision with four checks:
“Prototype in Python, ship in C” can be useful in some projects, but it is not a rule. A gateway, dashboard, data-cleaning tool, host test, and microcontroller firmware may each need a different language role. Review the evidence boundary instead of memorizing a fixed migration path.
18.7 Language Roles in an IoT Prototype
This section: Language Roles in an IoT Prototype — Most IoT prototypes are mixed-language systems even when the device firmware is small.
Most IoT prototypes are mixed-language systems even when the device firmware is small. A reliable review names the role each language is playing.
Inspect Figure 18.2 and ask where each language must run to produce faithful evidence for its assigned role.
Read Figure 18.2 from firmware to the host test rig. Register access, interrupts, boot, power modes, and fixed timing belong on the target. App state may share that target but has a different job. Gateway and cloud roles cross broker, storage, API, registry, and staff boundaries. Analysis code explores saved proof. A host rig can stress parsers and state machines at high speed. The language choice follows the role: first fix where the code runs and what proof it must yield, then weigh team skill and tool fit.
18.8 Constraint Gates
This section: Constraint Gates — Language selection becomes clearer when the team names the constraint gate.
Language selection becomes clearer when the team names the constraint gate. Some gates can be answered with a host script. Others require target hardware.
Inspect Figure 18.3 and ask which target constraint could veto a preferred programming language.
Read Figure 18.3 across timing, memory, power, safety, ecosystem, and team workflow before reaching target evidence. Deadlines and jitter test timing fit; heap, stack, buffers, and image size test memory. Current states test power, while fault recovery and input validation test safety. Libraries and vendor support must have a provable dependency path, and the team must be able to build, debug, review, and operate the result. A target run records what the chosen runtime actually proved.
Use these gates before choosing or changing language:
Do not compare languages with generic numbers. Build a tiny slice on the intended target and record the metric that actually decides the prototype: boot-to-sample time, heap headroom, payload encoding cost, sleep current, crash recovery, or reviewer productivity.
18.9 Common Language Families
This section: Common Language Families — Treat language families as evidence tools.
Treat language families as evidence tools. The exact language is less important than the runtime behavior, support boundary, and review trail.
18.10 Prototype-to-Production Handoff
This section: Prototype-to-Production Handoff — A fast language can reveal domain logic before the production runtime is ready.
A fast language can reveal domain logic before the production runtime is ready. That is useful only if the team records what transfers and what must be rebuilt.
On a Raspberry Pi, CPython is also a practical way to prove a small remote-logging slice: read a DHT-style temperature and humidity value, open a client socket to a known host and port, send a timestamped record, let the server write it to a file, then parse the stored rows for a quick plot. Treat that as gateway or lab evidence, not as final device proof. The record should include the sensor library version, GPIO or board pin used, socket family and transport choice, server bind address, file format, parsing rule such as delimiter splitting, and the matplotlib or notebook command that produced the chart.
Figure 18.4 separates prototype logic that transfers from runtime behavior that needs target testing.
The handoff in Figure 18.4 sorts a fast prototype by what can survive a runtime change. Algorithms, payload examples, calibration notes, test vectors, and state names can be kept when they do not depend on the original language. Drivers, sleep and wake code, interrupts, memory ownership, and update logic must be rebuilt for the target. The retest column asks for boot time, heap, stack, cycle time, radio timing, power state, watchdog behavior, and recovery on real hardware. Temporary scripts and fixed lab credentials are discarded. A Python gateway slice may explain domain logic, but its timing and device behavior are still target claims to measure.
Classify each artifact:
18.11 Worked Scenario: Cold-Room Monitor Logic
This section: Worked Scenario: Cold-Room Monitor Logic — A team is prototyping a cold-room monitor.
A team is prototyping a cold-room monitor. The device samples temperature and door state, records excursions, and sends a compact status payload to a gateway. The language decision is staged around evidence rather than around one permanent choice.
18.11.1 Stage 1: Learn the Sensor and Data Shape
Try the same temperature value with the door open, closed, and missing. If the intended excursion state differs, those are separate test vectors rather than one representative sample. Record the reason for each expected output so a later port preserves the rule, not merely the script’s current behaviour.
A high-level script on the bench explores calibration values, malformed readings, payload fields, and dashboard behavior. For the cold-room monitor, this work makes the relationship between temperature and door inputs, excursion states, and the compact status payload visible. Record accepted inputs, units, expected outputs, and failure responses so the threshold logic can be reviewed independently of the script.
Calibration notes, payload examples, and input/output test vectors provide reusable evidence for the later firmware implementation. They also give reviewers a way to compare behavior after a language change. The bench runtime does not establish final device power behavior, memory headroom, or wake timing. Those constraints need measurements in the target runtime before the early logic is accepted for device use.
18.11.2 Prove Target Firmware Limits
For example, measure boot-to-sample time with the real sensor connected, then repeat with it absent. The second run checks whether error handling changes the timing or leaves the startup sequence stuck. A normal boot measurement alone leaves that failure path open.
Build a small firmware slice in the target language and run it on the actual board profile. Exercise boot, sensor reading, state transition, payload encoding, the upload path, and a fault case. This connects the cold-room monitor logic to the hardware and runtime that must carry it, giving evidence beyond the bench script’s data and threshold checks.
Measure the constraint that decides whether the runtime fits: boot-to-sample time, sensor timing, heap headroom, stack use, payload encoding cost, or power behavior. Record the compiler, SDK, board profile, code version, and test command beside the results so the run can be repeated. State which limits were measured and which remain open; a working slice supports only the behaviors and conditions exercised.
18.11.4 Stage 4: Decide the Handoff
The team keeps the threshold rules, payload contract, and test vectors. It rewrites the driver, sleep behavior, storage path, update path, and failure recovery in the target runtime. The decision is supported by evidence, not by a blanket language preference.
prototype=cold-room-monitor
language_question=which runtime should carry each evidence risk?
quick_runtime=host script for calibration and payload experiments
target_runtime=firmware build for board profile, sleep, watchdog, and upload evidence
shared_artifacts=test vectors, payload examples, state names, calibration notes
must_check_on_target=boot time, heap headroom, power state, radio timing, update recovery
handoff=language-neutral logic kept; hardware-specific behavior rewritten and measured
18.12 Mixed-Language Boundary Record
This section: Mixed-Language Boundary Record — Mixed-language systems fail when teams do not record the boundary.
Mixed-language systems fail when teams do not record the boundary. A dashboard may assume a payload shape, a gateway script may hide a retry policy, or a firmware port may subtly change units.
Figure 18.5 gives each mixed-language interface an owner and a test contract.
The component field names the language, runtime, target, and evidence role of each side. The interface contract states accepted inputs and outputs and keeps shared test vectors that both implementations can run. A handoff owner must also name when the boundary needs retest, such as a serialization, compiler, or target change. That matters when a quick host prototype becomes embedded firmware: matching outputs on a small example do not prove timing, memory, or error handling on the device. Review the same normal and fault vectors across the boundary before reusing the prototype result.
Use this record when more than one language appears in the prototype:
prototype=
component=
language_or_runtime=
evidence_role=
interface_owned=
input_contract=
output_contract=
shared_test_vectors=
target_or_host_evidence=
known_runtime_limits=
secrets_or_configuration_source=
handoff_owner=
change_condition=
review_date=
18.13 Python Foundations for Device Prototypes
This section: Python Foundations for Device Prototypes — Python is useful in an IoT prototype when the first job is to make data and control flow visible.
Python is useful in an IoT prototype when the first job is to make data and control flow visible. A small program can hold numbers, strings, lists, tuples, and dictionaries; select a path with if/elif/else; repeat work with for or while; and stop or skip an iteration with break or continue. Functions give repeated work a name, accept arguments, return one or more values, and keep local state separate from values that the wider program owns.
The practical boundary is not syntax memorisation. Before a script is trusted, record its expected inputs, return values, state ownership, and the condition that ends every loop. That makes the same logic easier to test, move into a module, or hand to a target-runtime implementation later.
18.13.1 Modules, Exceptions, and Bounded Input
A module packages code or values for reuse. Import the whole module when its namespace makes ownership clear, or import a named item when the dependency is deliberately narrow. Runtime input still needs an explicit failure path: place the operation that may fail inside try, handle the expected exception in except, and use else only for work that should run after the guarded operation succeeds.
A prime-number check from the source deck illustrates the review pattern. The program converts user input to an integer, tests possible factors, stops after finding one, and reports whether the value is prime. The reusable lesson is to keep conversion failure, loop termination, and the returned decision visible rather than letting invalid input or an unbounded search disappear inside the function.
18.13.2 File, CSV, and Image Boundaries
File work has three explicit stages: open, read or write, and close. A with context keeps that lifecycle together so the file is closed when the operation finishes, including when an exception interrupts it. Text files carry unstructured records; the csv module reads and writes row-shaped data; and the PIL/Pillow image interface opens image files and supports inspection or transformations such as resize, rotation, and conversion between greyscale and RGB modes.
Treat each file as an interface. Record the path, mode, expected format, ownership of overwrite versus append, and the failure response before a prototype relies on it. For images, preserve the input format, dimensions, and colour mode alongside the transformed output so a later reviewer can tell what changed.
18.13.3 Minimal Socket Boundary
Imagine sending one timestamped temperature record from a device-side script to a lab server. Connection success tells you the endpoint was reachable. It does not show that the server understood the timestamp, accepted the unit, or stored the row where the plotting script expects it.
Define a known record and inspect it at both ends. Compare the sent fields with the received and stored fields, then repeat with unavailable-server and malformed-record cases. Keep connection errors distinct from rejected application data. This avoids a common leap from “bytes arrived” to “the reading was delivered correctly.” The minimal sequence below establishes the transport boundary; the record comparison supplies the additional evidence needed for the remote-logging claim.
Python’s socket interface exposes both connection-oriented and connectionless network paths. A minimal TCP server creates an Internet stream socket, binds it to an address and port, listens, accepts one connection, receives data, and closes the socket. Its client creates the matching socket, connects to the server address, and sends data.
That sequence proves only a narrow boundary: a client reached a listening endpoint and bytes crossed the connection. Keep the address, port, socket family, socket type, received-data limit, close behavior, and error path visible before treating the exchange as application delivery.
18.14 Knowledge Check
This section: Knowledge Check
18.15 Common Failure Patterns
This section: Common Failure Patterns
18.16 Summary
This section: Summary — First, Choose prototype languages from evidence needs, not from slogans or habit.
First, Choose prototype languages from evidence needs, not from slogans or habit. Next, Separate language roles across firmware, gateway, web, data, and test-harness surfaces. Then, Measure timing, memory, power, safety, and update behavior on the target when those constraints decide the language. After that, Keep language-neutral artifacts such as algorithms, payload examples, calibration notes, and test vectors. Finally, Record mixed-language boundaries so later environment, architecture, library, testing, OTA, and best-practice decisions can trust the handoff.
18.17 Key Takeaway
This section: Key Takeaway — Language choice should follow constraints such as memory, timing, libraries, safety, maintainability, team skill, and deployment target rather than fashion.
Language choice should follow constraints such as memory, timing, libraries, safety, maintainability, team skill, and deployment target rather than fashion.
18.18 What’s Next
This section: What’s Next
Find help
3 reviewed answers.
Where should I start in a module?
Open the module page from the sidebar to see its chapters in order. Use the module map to return to a topic you need.
Modules and pathsReviewed
What can I try when a project does not work?
Check the symptom, then work through the diagnostic steps. Keep the error message and the smallest example that reproduces it.
When you are stuckReviewed
How do interactive examples work?
Use the interactive guide to check how examples and simulations work before changing your project.
Interactive examples guideReviewed
