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:

PartMeaningExamples in your world
InputWhat enters the programA sensor state, webhook, CSV, button press, API response
StateWhat it remembers nowDatabase rows, HA helper values, variables, files
OutputWhat it changes or emitsA 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:

StructureUse it when you needPython example
List / arrayOrdered items, possibly repeateddevices = ["lamp", "fan"]
Dictionary / mapLookup by a meaningful keytemps["lounge"] = 21.4
SetUnique values and fast membership testsknown_ids = {"a1", "b2"}
TableRelated rows and columnsSQL 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:

LayerResponsibilityExample
InterfaceWhat a person/system sees and usesDashboard, web form, API endpoint
Application logicDecisions and workflows”If down for 60 sec, notify”
Data accessReads/writes persistent dataSQL queries, JSON files, HA state
InfrastructureRuntime environmentDocker, 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:

MethodMeaningExample
GETRead somethingFetch device state
POSTCreate / triggerCreate an automation
PUT / PATCHUpdateChange a record or setting
DELETERemoveDelete 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:

  1. Reproduce — make the failure happen predictably.
  2. Read the exact error — including the first relevant stack-trace line.
  3. State one hypothesis — e.g. “the entity ID is wrong”.
  4. Run the smallest test that can disprove it.
  5. Change one variable only.
  6. 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 levelQuestion it answersExample
Unit testDoes this small function behave correctly?Parse a date, calculate a budget
Integration testDo components work together?Python client writes expected DB row
End-to-end testCan a real user complete the workflow?Form submission creates a guide
Manual smoke testIs 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

NeedGood first choiceWhy
Automation, APIs, files, dataPythonClear, powerful ecosystem
Data questions and reportsSQLQueries live where the data lives
MicrocontrollersArduino C++ / ESP-IDF C++Hardware libraries and performance
ConfigurationYAMLDeclarative, human-readable formats
Browser UIHTML + CSS + JavaScriptThe native web platform
Infrastructure deliveryShell + Docker + GitDirect system control

Choose the tool that fits the problem, not the language that feels most impressive.

A practical build workflow

  1. Read the existing project README, config, and nearest similar feature.
  2. Run it unchanged to establish a baseline.
  3. Create a branch for a non-trivial change.
  4. Make the smallest vertical slice: one useful path from input to output.
  5. Add a test or repeatable manual check.
  6. Format, lint, and build before committing.
  7. Review your diff as if it came from someone else.
  8. Deploy deliberately and check logs/health afterwards.

Vocabulary you will meet constantly

TermPlain-English meaning
RuntimeThe environment while a program is actually running
DependencyA library or service your code needs
Package managerTool that installs/version-locks dependencies (uv, npm, pip)
Environment variableConfiguration supplied outside source code
BuildTurn source into a runnable/deployable output
CompileConvert C/C++ source to machine code before running
InterpreterRuns source directly (Python, JavaScript)
FrameworkA structured way to build a class of applications
LibraryReusable code you call when you choose
IdempotentSafe to run more than once with the same final result
RegressionA previously working behaviour that broke
Technical debtFuture 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.