Start here: what software development actually is
Software development is the disciplined process of turning an idea into a reliable, understandable change to a system. Writing code is only one part. The repeatable loop is:
Understand → design → make the smallest useful change → run it → test it → review it → ship it → observe it
For your projects, that might mean a Home Assistant automation, an n8n workflow, a Docker service, an Astro page, a Python script, or firmware for an ESP32. The tools change; the loop does not.
The central habit: reduce uncertainty before adding complexity. Read the existing system, make one small change, prove it works, then move on.
The mental model: inputs, state, outputs
Almost every program can be described with three things:
| Part | Meaning | Examples in your world |
|---|---|---|
| Input | What enters the program | A sensor state, webhook, CSV, button press, API response |
| State | What it remembers now | Database rows, HA helper values, variables, files |
| Output | What it changes or emits | A notification, dashboard update, motor command, web page |
When stuck, ask: What input arrived? What state did the system have? What output did I expect, and what output actually happened? That question solves an astonishing number of bugs.
The building blocks of code
Values and types
A value is data: 42, "Lounge", true, a date, or a list of devices. A type says how it behaves.
name = "John" # string
retry_count = 3 # integer
is_online = True # boolean
rooms = ["Lounge", "Office"] # list
Types prevent invalid operations. You can add numbers; you cannot meaningfully add a temperature to a device name. Modern languages help in different ways: Python checks much at runtime; C/C++ checks more before building; SQL has types in the database schema.
Control flow
Control flow decides what runs:
if internet_reachable:
send_status("WAN healthy")
else:
send_alert("WAN down")
The core patterns are if / else, loops (for, while), and early exits (return, break). Prefer simple, readable conditions over clever compressed logic.
Functions
A function is a named, reusable operation with a clear contract:
def format_temperature(celsius: float) -> str:
return f"{celsius:.1f} °C"
Good functions do one job, take explicit inputs, and return a useful result. A short function with a good name is better documentation than a long comment.
Data structures
Use the structure that matches the question:
| Structure | Use it when you need | Python example |
|---|---|---|
| List / array | Ordered items, possibly repeated | devices = ["lamp", "fan"] |
| Dictionary / map | Lookup by a meaningful key | temps["lounge"] = 21.4 |
| Set | Unique values and fast membership tests | known_ids = {"a1", "b2"} |
| Table | Related rows and columns | SQL database table |
Design before code
You do not need a big document. For a feature, write five lines first:
Goal: Start the lounge heater at the scheduled time.
Trigger: input_datetime changes or time matches it.
Inputs: desired time, climate.lounge state.
Success: heater is heating at the requested set point and a notification is sent.
Failure: invalid time, offline device, or action error is logged/notified.
This is a specification. It turns a vague desire into observable behaviour.
Acceptance criteria
Before implementation, define what proves success:
- A scheduled time of 05:00 starts the heater at 05:00.
- It does not start when the automation is disabled.
- A manual run behaves consistently.
- The user receives one notification, not three.
If you cannot state how to test it, the feature is not designed yet.
Architecture: keep responsibilities separate
Useful software separates concerns:
| Layer | Responsibility | Example |
|---|---|---|
| Interface | What a person/system sees and uses | Dashboard, web form, API endpoint |
| Application logic | Decisions and workflows | ”If down for 60 sec, notify” |
| Data access | Reads/writes persistent data | SQL queries, JSON files, HA state |
| Infrastructure | Runtime environment | Docker, reverse proxy, OS, network |
Avoid mixing all of these in one giant function or YAML automation. Separation makes testing and future changes much cheaper.
APIs and integrations
An API is a contract between systems. Most web APIs use HTTP:
| Method | Meaning | Example |
|---|---|---|
GET | Read something | Fetch device state |
POST | Create / trigger | Create an automation |
PUT / PATCH | Update | Change a record or setting |
DELETE | Remove | Delete a stale resource |
API calls commonly send or receive JSON. Always check the HTTP status, inspect the response body on failure, and never put API keys in source code or screenshots. Use .env files, secret stores, or platform-managed secrets.
Debugging: a reliable method
Do not guess repeatedly. Debug like a scientist:
- Reproduce — make the failure happen predictably.
- Read the exact error — including the first relevant stack-trace line.
- State one hypothesis — e.g. “the entity ID is wrong”.
- Run the smallest test that can disprove it.
- Change one variable only.
- Verify the original behaviour, then add a regression test or safeguard.
Your first debugging tools
# See a command's exit status
command_here; echo $?
# Follow service logs
journalctl -u service-name -f
docker logs -f container-name
# Inspect JSON safely
curl -s http://example/api | jq .
# Test a Python module
python -m pytest -q
A log line should answer: what happened, to which thing, with which important values, and what happened next?
Testing: confidence, not ceremony
| Test level | Question it answers | Example |
|---|---|---|
| Unit test | Does this small function behave correctly? | Parse a date, calculate a budget |
| Integration test | Do components work together? | Python client writes expected DB row |
| End-to-end test | Can a real user complete the workflow? | Form submission creates a guide |
| Manual smoke test | Is the deployed system alive? | Open page, run automation, check log |
Test the edge cases, not only the happy path: empty values, network failures, duplicate events, unexpected formats, daylight-saving transitions, and recovery after restart.
Version control is your safety net
Git records deliberate checkpoints. The smallest safe loop is:
git status
git diff
git add path/to/changed-file
git commit -m "feat: describe the outcome"
git push
Commit working, coherent changes. Never commit secrets, generated credentials, database dumps, .env files, or local build junk. See the Git & GitHub Practical Workflow guide for the complete routine.
How to choose a language
| Need | Good first choice | Why |
|---|---|---|
| Automation, APIs, files, data | Python | Clear, powerful ecosystem |
| Data questions and reports | SQL | Queries live where the data lives |
| Microcontrollers | Arduino C++ / ESP-IDF C++ | Hardware libraries and performance |
| Configuration | YAML | Declarative, human-readable formats |
| Browser UI | HTML + CSS + JavaScript | The native web platform |
| Infrastructure delivery | Shell + Docker + Git | Direct system control |
Choose the tool that fits the problem, not the language that feels most impressive.
A practical build workflow
- Read the existing project README, config, and nearest similar feature.
- Run it unchanged to establish a baseline.
- Create a branch for a non-trivial change.
- Make the smallest vertical slice: one useful path from input to output.
- Add a test or repeatable manual check.
- Format, lint, and build before committing.
- Review your diff as if it came from someone else.
- Deploy deliberately and check logs/health afterwards.
Vocabulary you will meet constantly
| Term | Plain-English meaning |
|---|---|
| Runtime | The environment while a program is actually running |
| Dependency | A library or service your code needs |
| Package manager | Tool that installs/version-locks dependencies (uv, npm, pip) |
| Environment variable | Configuration supplied outside source code |
| Build | Turn source into a runnable/deployable output |
| Compile | Convert C/C++ source to machine code before running |
| Interpreter | Runs source directly (Python, JavaScript) |
| Framework | A structured way to build a class of applications |
| Library | Reusable code you call when you choose |
| Idempotent | Safe to run more than once with the same final result |
| Regression | A previously working behaviour that broke |
| Technical debt | Future cost created by a shortcut now |
30-day learning path
Week 1 — Foundations: work through this guide; use Git for a small change; learn to read logs and errors.
Week 2 — Python + shell: automate one repetitive local task, parse a JSON response, and write a small test.
Week 3 — Data + configuration: query a sample SQLite database with SQL; build a Docker Compose or HA YAML change and validate it.
Week 4 — Interface or device: make a simple HTML/JS page or flash a small ESP32 sensor project. Document what you learned in the repo README.
When stuck: the 60-second checklist
1. What exact command/action failed?
2. What is the exact error or log line?
3. Did it work before? What changed?
4. What input/config/state was involved?
5. Can I create a smaller reproduction?
6. What official docs or existing project example is closest?
The goal is not to memorise every command. It is to build a system for finding the next correct move.