Python
Dictionary
Class
Data Cleaning
Data Structures

How to create dict from class without None fields?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When converting Python objects to dictionaries, None values often create noisy payloads and can change API behavior in unexpected ways. In some systems, sending a key with None means "clear this value," while omitting the key means "leave it unchanged." A robust solution should make that rule explicit and apply it consistently across simple and nested objects.

Pick the Right Serialization Style

There are three common patterns:

  • Plain class using __dict__
  • 'dataclass using asdict'
  • Model library with built-in exclude options

For lightweight projects, plain class or dataclass is enough. The main requirement is a predictable filter step that removes only None, not valid falsy values like 0, False, or an empty string.

A minimal class-based approach:

python
1class UserPatch:
2    def __init__(self, name=None, age=None, active=None):
3        self.name = name
4        self.age = age
5        self.active = active
6
7    def to_dict(self):
8        # Keep False and 0. Remove only None.
9        return {k: v for k, v in self.__dict__.items() if v is not None}
10
11payload = UserPatch(name="Ava", age=0, active=False).to_dict()
12print(payload)
13# Expected: {'name': 'Ava', 'age': 0, 'active': False}

This is often good enough for flat objects.

Dataclass Version With Stronger Structure

dataclass gives clearer field definitions and better tooling support. You can still apply the same filter rule after asdict.

python
1from dataclasses import dataclass, asdict
2
3@dataclass
4class ProductUpdate:
5    title: str | None = None
6    price: float | None = None
7    in_stock: bool | None = None
8
9    def to_dict(self):
10        raw = asdict(self)
11        return {k: v for k, v in raw.items() if v is not None}
12
13update = ProductUpdate(title="Notebook", in_stock=False)
14print(update.to_dict())
15# Expected: {'title': 'Notebook', 'in_stock': False}

This keeps the implementation explicit and easy to test.

Handling Nested Objects and Collections

Most real payloads are nested. A one-level comprehension will not remove None values inside lists, dictionaries, or nested objects. For that, use a recursive cleaner.

python
1from dataclasses import dataclass, asdict, is_dataclass
2
3
4def strip_none(value):
5    if is_dataclass(value):
6        value = asdict(value)
7
8    if isinstance(value, dict):
9        cleaned = {}
10        for k, v in value.items():
11            nested = strip_none(v)
12            if nested is not None:
13                cleaned[k] = nested
14        return cleaned
15
16    if isinstance(value, list):
17        return [item for item in (strip_none(i) for i in value) if item is not None]
18
19    return value
20
21
22@dataclass
23class Address:
24    city: str | None = None
25    postal_code: str | None = None
26
27
28@dataclass
29class ProfileUpdate:
30    email: str | None = None
31    address: Address | None = None
32    tags: list[str | None] | None = None
33
34
35obj = ProfileUpdate(
36    email="[email protected]",
37    address=Address(city="Toronto", postal_code=None),
38    tags=["backend", None, "python"],
39)
40
41print(strip_none(obj))
42# Expected: {'email': '[email protected]', 'address': {'city': 'Toronto'}, 'tags': ['backend', 'python']}

This approach keeps nested payloads clean while preserving valid falsy values.

API Patch Semantics: Omit vs Null

Before you finalize your serializer, confirm contract semantics with API consumers.

  • Omitted key means no change.
  • Key present with None means clear existing value.

If your API depends on that difference, do not blindly remove None everywhere. Sometimes you need a method that supports both modes.

python
1class Patch:
2    def __init__(self, nickname=None, bio=None):
3        self.nickname = nickname
4        self.bio = bio
5
6    def to_dict(self, include_nulls=False):
7        raw = self.__dict__
8        if include_nulls:
9            return raw.copy()
10        return {k: v for k, v in raw.items() if v is not None}
11
12p = Patch(nickname=None, bio="new bio")
13print(p.to_dict(include_nulls=False))
14print(p.to_dict(include_nulls=True))

One explicit flag is safer than hidden behavior.

Testing Strategy That Prevents Regressions

Create focused tests around filtering rules. Verify the following cases:

  • 'None values are removed.'
  • '0, False, and empty string are preserved.'
  • Nested dictionaries and lists are cleaned correctly.
  • Optional mode that keeps nulls works as expected.

Small tests catch subtle breakage quickly when models evolve.

Common Pitfalls

  • Using if v instead of if v is not None, which accidentally removes 0 and False.
  • Cleaning only top-level keys and leaving nested None values untouched.
  • Applying one global rule when different API endpoints need different null semantics.
  • Mutating the original object during filtering, which creates hard-to-track side effects.
  • Skipping tests for falsy values and nested collections.

Summary

  • Remove None values with an explicit filter rule, not generic truthy checks.
  • For flat objects, __dict__ or asdict plus a comprehension is sufficient.
  • For nested payloads, use a recursive cleaner that handles dicts and lists.
  • Confirm API meaning of omitted keys versus null keys before finalizing behavior.
  • Add tests for falsy values and nested structures to keep serialization stable.

Course illustration
Course illustration

All Rights Reserved.