Add Helm Values for Python Config

Learn to add Helm values for Python environment config in this Kubernetes for Python Developers tutorial — hands-on steps, troubleshooting, and what to study next.

Focus: add helm values for python environment config

Sponsored

You’ve containerized your Python service, pushed it to a registry, and deployed it to Kubernetes—only to realize that every environment (dev, staging, prod) needs different values: DATABASE_URL, DEBUG, logging levels, feature flags. Hardcoding them forces rebuilds and invites disaster. This is exactly the pain that Helm values solve: parameterizing your chart so the same Python app template can be deployed safely to any environment by simply swapping a values file. Let’s learn how to add Helm values for Python environment config and never rebuild an image just to change a setting again.

The problem this lesson solves

Your Python application’s environment-specific configuration is probably scattered: a config.py with hardcoded constants, a .env file that must be copied around, or a ConfigMap you keep editing after the fact. None of these scale. Change DATABASE_URL in production, and you either rebuild the image (slow, risky) or you manually patch a live secret while hoping no one watches.

Kubernetes handles config through ConfigMap and Secret objects, but creating and managing those per environment is repetitive and error-prone. Helm brings a better way: define your Python app’s env vars in one template, and let Helm fill the gaps with values—a single source of truth per environment.

If you’ve ever pushed a Python commit that simply tweaks a URL or toggles a debug flag, you’ve felt this pain. With Helm values, that change becomes a one-line edit in values-prod.yaml, followed by helm upgrade—no commit, no CI run, no image rebuild.

Core concept / mental model

Think of a Helm chart as a parameterized recipe for your Python app. The template is the cooking steps (deployment.yaml, service.yaml), and the values file is the list of ingredients. You reuse the same recipe for every environment, but swap the ingredient list.

  • Chart templates: JINJA-like Go templates that contain placeholders like {{ .Values.foo }}.
  • Values files: YAML files that supply the fetched values (values.yaml for defaults, values-prod.yaml for overrides).
  • Scope: {{ .Values.myApp.replicas }} lets you group related settings like a Python module.

In our case, the template will loop over a dictionary of env vars and produce the exact env: list for the container—turning a static block into a dynamic map. This is the Kubernetes-native way to inject configuration into the pod’s environment.

How it works step by step

1. Start with your Python app’s env needs

Every Python service reads settings from environment variables—typically via os.getenv("DATABASE_URL") or a config library like pydantic / python-dotenv. List those variables first.

2. Extend values.yaml

Add an env dictionary under a top-level key (e.g., app). Keep it simple:

# values.yaml
app:
  env:
    DEBUG: "false"
    LOG_LEVEL: "INFO"

3. Update the deployment template

Inside templates/deployment.yaml, replace the hardcoded env: block with a range loop that generates each variable.

4. Create environment-specific overrides

For production, create values-prod.yaml with the prod DB URL and token flags.

5. Deploy with -f

Use helm install or helm upgrade with the override file. Helm merges the base values with the override—no duplication.

Hands-on walkthrough

Let’s build a real example. Assume you have a fastapi-app chart with a Deployment that currently hardcodes one env var.

Step 1: Current template (before)

# templates/deployment.yaml (before)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.app.name }}
spec:
  template:
    spec:
      containers:
        - name: api
          image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
          env:
            - name: DATABASE_URL
              value: "postgresql://localhost:5432/dev"

Step 2: Add values structure

# values.yaml
app:
  name: fastapi-app
  env:
    DEBUG: "false"
    LOG_LEVEL: "INFO"
image:
  repository: ghcr.io/yourname/fastapi-app
  tag: "1.2.3"

Step 3: Update the template

# templates/deployment.yaml (after)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.app.name }}
spec:
  template:
    spec:
      containers:
        - name: api
          image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
          env:
          {{- range $key, $value := .Values.app.env }}
            - name: {{ $key }}
              value: {{ $value | quote }}
          {{- end }}

Step 4: Create production overrides

# values-prod.yaml
app:
  env:
    DEBUG: "false"
    LOG_LEVEL: "WARNING"
    DATABASE_URL: "postgresql://prod-user:${PROD_DB_PASSWORD}@prod-db.internal:5432/proddb"

Pro tip: Never put raw secrets in plain-text values files. Reference them via valueFrom in the template and wire them to a Kubernetes Secret. We'll explore that in the next lesson.

Step 5: Deploy per environment

# dev (defaults)
helm install my-app ./fastapi-app

# prod (override)
helm upgrade my-app ./fastapi-app -f values-prod.yaml --namespace production

When you run kubectl get deployment my-app -o yaml in the production namespace, you'll see the env block contains DATABASE_URL, LOG_LEVEL: "WARNING", and DEBUG: "false"—pulled directly from your values file. No image rebuild.

Compare options / when to choose what

Helm values aren't the only way to inject configuration. Here’s how they stack up:

Approach Pros Cons Best for
Helm values One template, many environments; declarative; EUID-friendly; easy helm diff Adds Helm as a dependency; values can become complex if not structured well Most Python services deployed in any real cluster
ConfigMap + kubectl patch Kubernetes-native, no Helm required Manual patches per environment; no versioning or rollback; error-prone Quick experiments, single-namespace apps
Hardcoding in image Simplest Requires rebuild for every change; risky across environments Demos, small toys, never production
External config service (Consul, etc.) Dynamic updates, no redeploy Extra infrastructure; more moving parts Large microservices with Vault integration

When to choose what: If you already use Helm (or plan to—this track assumes it), helm values is the natural, repeatable choice. If you need dynamic reload without any chart structure, a config service wins. For everything else, Helm’s --set and override files hit the sweet spot.

Troubleshooting & edge cases

1. nil pointer evaluating interface {}.app.env — missing values

Helm can’t find the nested key. You forgot to define it in values.yaml or mis-typed the path.

  • Fix: Ensure app: and env: exist in values.yaml. Run helm template to see the generated YAML.

2. Number values are unquoted, causing YAML type issues

If you use value: {{ $value }} without | quote, a value of 123 becomes a number, but Python expects a string.

  • Fix: Always quote with {{ $value | quote }} (or use tpl if you need expression evaluation).

3. Key names with dashes or special chars

YAML keys like my-db-url are awkward inside templates. Helm template functions like nindent and quote won’t fix invalid env var names.

  • Fix: Use explicit name and value pairs and only permit [A-Za-z0-9_] in key names.

4. Secrets accidentally logged in values

Helm includes all values in the rendered chart, so putting DATABASE_PASSWORD directly leaks it into pod specs and logs.

  • Fix: Use valueFrom and secretKeyRef instead of plain strings for sensitive data.

5. Override files not applied

helm upgrade without -f forgets previous overrides. Helm doesn’t persist runtime values by default.

  • Fix: Always pass the same -f files on each upgrade, or use --set sparingly for one-off tweaks.

What you learned & what's next

You’ve now mastered the core pattern: define a values-driven env block in your Deployment template so your Python app reads configuration from environment variables—without rebuilding an image. You can maintain per-environment values-*.yaml files for dev, staging, and prod, and confidently run helm upgrade to roll out changes.

This lesson tied directly to the learning objective of adding Helm values for Python environment config and hands-on practice. As you move forward, the next natural topic is managing secrets with Helm and Kubernetes Secrets—learn how to wire DATABASE_URL’s password from a secretKeyRef so your values files stay safe and your Python app never receives raw secrets in plain text. You’ll extend the same range pattern to mount secret variables and keep your configuration both dynamic and secure.

Practice recap

Extend the deployment template from this lesson by adding valueFrom for a DATABASE_PASSWORD using a secretKeyRef, then create values-staging.yaml with DEBUG: "true" and run helm upgrade -f values-staging.yaml to verify. Finally, use kubectl exec into the running pod and print os.getenv('DEBUG') to confirm the injection works.

Common mistakes

  • Forgetting to quote values in the template, producing unquoted numbers or booleans that Python misreads as int or bool when it expects strings.
  • Putting secrets directly in the values file and then committing it to git, leaking production credentials.
  • Skipping the -f override on helm upgrade, so environment-specific changes silently revert to defaults.
  • Using a key with a hyphen in the values file but a template that assumes [A-Za-z0-9_], causing invalid env var names in the pod.

Variations

  1. Use --set app.env.DATABASE_URL=... on the CLI for one-off changes without a separate values file.
  2. Store values in external yaml files per environment (e.g., values-prod.yaml) and reference them via -f for full reviewability.
  3. Leverage Helm's tpl function if you need to evaluate expressions inside values (e.g., {{ tpl .Values.app.env.DATABASE_URL . }}).

Real-world use cases

  • A Python FastAPI backend deployed to dev, staging, and prod with different database URLs and DEBUG flags via per-environment values files.
  • A Django app using os.getenv for SECRET_KEY and DJANGO_SETTINGS_MODULE, injected via Helm values and a Secret for the key.
  • A Python Celery worker that needs different BROKER_URL and CELERY_QUEUE values per environment, seamlessly switched by helm upgrade -f.

Key takeaways

  • Helm values decouple environment configuration from your Python image, so you never rebuild for a simple env change.
  • Use a range loop over a dictionary in values.yaml to generate the env: block in your Deployment template.
  • Quote every env value in the template to keep Python receiving strings, even for numbers or booleans.
  • Create one override file per environment (values-prod.yaml, values-staging.yaml) and pass -f on every helm upgrade.
  • Never store secrets in plain values—use valueFrom and secretKeyRef to wire them into the pod.
  • Run helm template before applying to catch template errors early and inspect the exact output.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.