Repository: streamlit/streamlit
Pull request: Allow `None` as value for `st.number_input`
## Describe your changes

This PR supports setting `None` as the value in `st.number_input`. Using `value=None` will render an empty number input that returns `None` as long as the user hasn't specified a number:

<img width="599" alt="image" src="https://github.com/streamlit/streamlit/assets/2852129/5fca5660-4c3c-4503-9472-c83e6559736c">

If a user has specified a number, the user can clear this value again to reset the input to `None`:

<img width="599" alt="image" src="https://github.com/streamlit/streamlit/assets/2852129/073ccda3-0f63-493e-a0d6-dbea8cfc5715">

- Spec: https://www.notion.so/snowflake-corp/Spec-v2-b00de8d56f99464693e7ddfdea0674e9

## GitHub Issue Link (if applicable)

Closes https://github.com/streamlit/streamlit/issues/4650

## Testing Plan

- Added e2e tests and unit tests for value=None behaviour.

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.


Problem statement:
Number input does not accept None as value parameter
### Summary

It is not possible to set the value parameter of the number_input to None, even though it is listed in the API as possible and it works for all other input widgets.

### Steps to reproduce
```
import streamlit as st
input = st.number_input('Number input', value = None)
```

**Expected behavior:**

Shows the number input widget without a set default value, same as if it was not set at all.

**Actual behavior:**

Throws the following error:
streamlit.errors.StreamlitAPIException: Default value for number_input should be an int or a float.        

### Is this a regression?

Don't know

### Debug info

- Streamlit version: 1.8.1
- Python version: 3.9.12
- Streamlit installed with pip
- OS version: Windows 10
- Browser version: Chrome

Hints from issue discussion:
Hi @ChristophBoerlin  👋🏼 
Appreciate your submission, and for helping to make Streamlit better! I was able to confirm the conflict in behavior vs. our `st.number_input` documentation 😓 
We will take a look at how we can incorporate this fix into our future development plans. Thanks again!
any update on this?
+1
Is there an update on this?  The behavior is still happening.

Yet happening although docs specify it can be None
Hi, I would like to volunteer to work on this issue and I appreciate any suggestions!
Hey all! We don't want to fix this for now since we are thinking about enabling setting `None` *as a feature*. I.e. you could set `value=None` and the widget would not show its current default value, but stay completely empty and also return `None`. Unfortunately, that has a few implications that we'd need to think through first (especially how this works on other widgets). So no promises on when we'll get to it.

Commit message: Update docstring

Patch to review:
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-clearable_input[chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-clearable_input[chromium].png
new file mode 100644
index 000000000000..4b982bae951c
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-clearable_input[chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-clearable_input[firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-clearable_input[firefox].png
new file mode 100644
index 000000000000..98523e8ce233
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-clearable_input[firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-clearable_input[webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-clearable_input[webkit].png
new file mode 100644
index 000000000000..3b12c5a07c53
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-clearable_input[webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[dark_theme-chromium].png
new file mode 100644
index 000000000000..9dcc9472ab18
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[dark_theme-firefox].png
new file mode 100644
index 000000000000..188d38c75f94
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[dark_theme-webkit].png
new file mode 100644
index 000000000000..fe4f2a11a4a8
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[light_theme-chromium].png
new file mode 100644
index 000000000000..b8d16e6dbe36
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[light_theme-firefox].png
new file mode 100644
index 000000000000..050fb4038b25
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[light_theme-webkit].png
new file mode 100644
index 000000000000..5cd9c3d6ba44
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-default[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[dark_theme-chromium].png
new file mode 100644
index 000000000000..32caeae70246
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[dark_theme-firefox].png
new file mode 100644
index 000000000000..e9181b68992c
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[dark_theme-webkit].png
new file mode 100644
index 000000000000..c04ae84f875d
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[light_theme-chromium].png
new file mode 100644
index 000000000000..f7c25df2dd1c
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[light_theme-firefox].png
new file mode 100644
index 000000000000..fa7bf7c1c881
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[light_theme-webkit].png
new file mode 100644
index 000000000000..7121d18a6ef9
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-disabled_true[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-input_instructions[chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-input_instructions[chromium].png
new file mode 100644
index 000000000000..ecac8be36ac6
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-input_instructions[chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-input_instructions[firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-input_instructions[firefox].png
new file mode 100644
index 000000000000..26c46e64ce9d
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-input_instructions[firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-input_instructions[webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-input_instructions[webkit].png
new file mode 100644
index 000000000000..88a9f341e3ef
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-input_instructions[webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[dark_theme-chromium].png
new file mode 100644
index 000000000000..6d019908ec7c
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[dark_theme-firefox].png
new file mode 100644
index 000000000000..db59e42bcecd
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[dark_theme-webkit].png
new file mode 100644
index 000000000000..461443a3b101
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[light_theme-chromium].png
new file mode 100644
index 000000000000..c6b2cb9734f7
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[light_theme-firefox].png
new file mode 100644
index 000000000000..f65bf1b362f7
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[light_theme-webkit].png
new file mode 100644
index 000000000000..8de9fe95a053
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_collapsed[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[dark_theme-chromium].png
new file mode 100644
index 000000000000..0a53dc3bb749
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[dark_theme-firefox].png
new file mode 100644
index 000000000000..adf81b0d880b
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[dark_theme-webkit].png
new file mode 100644
index 000000000000..16f1c563aa5e
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[light_theme-chromium].png
new file mode 100644
index 000000000000..75755a259008
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[light_theme-firefox].png
new file mode 100644
index 000000000000..80cdc7c41f8a
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[light_theme-webkit].png
new file mode 100644
index 000000000000..2df9ba2619ba
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-label_hidden[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[dark_theme-chromium].png
new file mode 100644
index 000000000000..c8141985991d
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[dark_theme-firefox].png
new file mode 100644
index 000000000000..0c3d80fa9dbd
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[dark_theme-webkit].png
new file mode 100644
index 000000000000..0ab83730cd37
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[light_theme-chromium].png
new file mode 100644
index 000000000000..09b4c8dc5567
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[light_theme-firefox].png
new file mode 100644
index 000000000000..6e778df21372
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[light_theme-webkit].png
new file mode 100644
index 000000000000..a2a840ca4070
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-max_10[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[dark_theme-chromium].png
new file mode 100644
index 000000000000..4d92957069cd
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[dark_theme-firefox].png
new file mode 100644
index 000000000000..736adc4cf5d2
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[dark_theme-webkit].png
new file mode 100644
index 000000000000..149f29c4697f
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[light_theme-chromium].png
new file mode 100644
index 000000000000..459403414c6e
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[light_theme-firefox].png
new file mode 100644
index 000000000000..601b3e8da1ad
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[light_theme-webkit].png
new file mode 100644
index 000000000000..098283b4d8cf
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-min_max[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[dark_theme-chromium].png
new file mode 100644
index 000000000000..1f2491f93990
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[dark_theme-firefox].png
new file mode 100644
index 000000000000..3e2a095a2add
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[dark_theme-webkit].png
new file mode 100644
index 000000000000..dc09457b3ee7
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[light_theme-chromium].png
new file mode 100644
index 000000000000..a34d6e72da0e
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[light_theme-firefox].png
new file mode 100644
index 000000000000..5cf32ebe053e
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[light_theme-webkit].png
new file mode 100644
index 000000000000..36fb7c5985f0
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-on_change[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[dark_theme-chromium].png
new file mode 100644
index 000000000000..31128c130a85
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[dark_theme-firefox].png
new file mode 100644
index 000000000000..b661d5443688
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[dark_theme-webkit].png
new file mode 100644
index 000000000000..2eb72fa69163
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[light_theme-chromium].png
new file mode 100644
index 000000000000..bdb6991a2244
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[light_theme-firefox].png
new file mode 100644
index 000000000000..81d22ea9fd79
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[light_theme-webkit].png
new file mode 100644
index 000000000000..66d947c40d96
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-small_width[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[dark_theme-chromium].png
new file mode 100644
index 000000000000..db02c5aec30e
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[dark_theme-firefox].png
new file mode 100644
index 000000000000..25fba1ae21e2
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[dark_theme-webkit].png
new file mode 100644
index 000000000000..14392cfa9e98
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[light_theme-chromium].png
new file mode 100644
index 000000000000..4475d5a6f2db
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[light_theme-firefox].png
new file mode 100644
index 000000000000..d35bebb215e4
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[light_theme-webkit].png
new file mode 100644
index 000000000000..80f35e2216e8
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-step_2[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[dark_theme-chromium].png
new file mode 100644
index 000000000000..d0b60618f284
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[dark_theme-firefox].png
new file mode 100644
index 000000000000..1ac0ac91beb1
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[dark_theme-webkit].png
new file mode 100644
index 000000000000..357f58a1f91f
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[light_theme-chromium].png
new file mode 100644
index 000000000000..6fc244db379d
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[light_theme-firefox].png
new file mode 100644
index 000000000000..58b60db10da8
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[light_theme-webkit].png
new file mode 100644
index 000000000000..8aee1336d839
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_1[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[dark_theme-chromium].png
new file mode 100644
index 000000000000..db2f54ab529b
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[dark_theme-firefox].png
new file mode 100644
index 000000000000..a87a03181843
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[dark_theme-webkit].png
new file mode 100644
index 000000000000..6381054ee5ce
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[light_theme-chromium].png
new file mode 100644
index 000000000000..2982b30cd999
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[light_theme-firefox].png
new file mode 100644
index 000000000000..62e766f5eaba
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[light_theme-webkit].png
new file mode 100644
index 000000000000..187e8e0ec2c9
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none[light_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[dark_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[dark_theme-chromium].png
new file mode 100644
index 000000000000..34906bad439a
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[dark_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[dark_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[dark_theme-firefox].png
new file mode 100644
index 000000000000..f0e29905ec66
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[dark_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[dark_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[dark_theme-webkit].png
new file mode 100644
index 000000000000..aa1019956e40
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[dark_theme-webkit].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[light_theme-chromium].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[light_theme-chromium].png
new file mode 100644
index 000000000000..02a1ca81f36d
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[light_theme-chromium].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[light_theme-firefox].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[light_theme-firefox].png
new file mode 100644
index 000000000000..ebb3394bfd64
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[light_theme-firefox].png differ
diff --git a/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[light_theme-webkit].png b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[light_theme-webkit].png
new file mode 100644
index 000000000000..d9bbf698c946
Binary files /dev/null and b/e2e_playwright/__snapshots__/linux/st_number_input_test/st_number_input-value_none_min_1[light_theme-webkit].png differ
diff --git a/e2e_playwright/st_code_test.py b/e2e_playwright/st_code_test.py
index f08c75dbbb82..1b120487c7d1 100644
--- a/e2e_playwright/st_code_test.py
+++ b/e2e_playwright/st_code_test.py
@@ -47,7 +47,7 @@ def test_code_blocks_render_correctly(
 def test_code_in_markdown(app: Page, assert_snapshot: ImageCompareFunction):
     """Test that the syntax highlighting is applied correctly to the code block."""
     # This test seems to be a little flaky without additional timeout:
-    app.wait_for_timeout(250)
+    app.wait_for_timeout(500)
 
     expander_with_code = app.get_by_test_id("stExpander")
 
diff --git a/e2e_playwright/st_number_input.py b/e2e_playwright/st_number_input.py
new file mode 100644
index 000000000000..1b688f6c4f74
--- /dev/null
+++ b/e2e_playwright/st_number_input.py
@@ -0,0 +1,74 @@
+# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import streamlit as st
+from streamlit import runtime
+
+i1 = st.number_input("number input 1 (default)")
+st.write("number input 1 (default) - value: ", i1)
+
+i2 = st.number_input("number input 2 (value=1)", value=1)
+st.write("number input 2 (value=1) - value: ", i2)
+
+i3 = st.number_input("number input 3 (min & max)", 1, 10)
+st.write("number input 3 (min & max) - value: ", i3)
+
+i4 = st.number_input("number input 4 (step=2)", step=2)
+st.write("number input 4 (step=2) - value: ", i4)
+
+i5 = st.number_input("number input 5 (max=10)", max_value=10)
+st.write("number input 5 (max=10) - value: ", i5)
+
+i6 = st.number_input("number input 6 (disabled=True)", disabled=True)
+st.write("number input 6 (disabled=True) - value: ", i6)
+
+i7 = st.number_input("number input 7 (label=hidden)", label_visibility="hidden")
+st.write("number input 7 (label=hidden) - value: ", i7)
+
+i8 = st.number_input("number input 8 (label=collapsed)", label_visibility="collapsed")
+st.write("number input 8 (label=collapsed) - value: ", i8)
+
+if runtime.exists():
+
+    def on_change():
+        st.session_state.number_input_changed = True
+
+    st.number_input(
+        "number input 9 (on_change)", key="number_input9", on_change=on_change
+    )
+    st.write("number input 9 (on_change) - value: ", st.session_state.number_input9)
+    st.write(
+        "number input 9 (on_change) - changed:",
+        "number_input_changed" in st.session_state,
+    )
+
+[col1, col2, col3, col4, col5, col6] = st.columns(6)
+
+with col1:
+    i10 = st.number_input("number input 10 (small width)", max_value=10)
+    st.write("number input 10 (small width) - value: ", i10)
+
+i11 = st.number_input("number input 11 (value=None)", value=None)
+st.write("number input 11 (value=None) - value: ", i11)
+
+if "number_input12" not in st.session_state:
+    st.session_state["number_input12"] = 10
+
+i12 = st.number_input(
+    "number input 12 (value from state & min=1)",
+    value=None,
+    min_value=1,
+    key="number_input12",
+)
+st.write("number input 12 (value from state & min=1) - value: ", i12)
diff --git a/e2e_playwright/st_number_input_test.py b/e2e_playwright/st_number_input_test.py
new file mode 100644
index 000000000000..9bdfb0d66544
--- /dev/null
+++ b/e2e_playwright/st_number_input_test.py
@@ -0,0 +1,179 @@
+# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from playwright.sync_api import Page, expect
+
+from e2e_playwright.conftest import ImageCompareFunction
+
+
+def test_number_input_widget_display(
+    themed_app: Page, assert_snapshot: ImageCompareFunction
+):
+    """Test that st.number_input renders correctly."""
+    number_input_elements = themed_app.get_by_test_id("stNumberInput")
+    expect(number_input_elements).to_have_count(12)
+
+    assert_snapshot(number_input_elements.nth(0), name="st_number_input-default")
+    assert_snapshot(number_input_elements.nth(1), name="st_number_input-value_1")
+    assert_snapshot(number_input_elements.nth(2), name="st_number_input-min_max")
+    assert_snapshot(number_input_elements.nth(3), name="st_number_input-step_2")
+    assert_snapshot(number_input_elements.nth(4), name="st_number_input-max_10")
+    assert_snapshot(number_input_elements.nth(5), name="st_number_input-disabled_true")
+    assert_snapshot(number_input_elements.nth(6), name="st_number_input-label_hidden")
+    assert_snapshot(
+        number_input_elements.nth(7), name="st_number_input-label_collapsed"
+    )
+    assert_snapshot(number_input_elements.nth(8), name="st_number_input-on_change")
+    assert_snapshot(number_input_elements.nth(9), name="st_number_input-small_width")
+    assert_snapshot(number_input_elements.nth(10), name="st_number_input-value_none")
+    assert_snapshot(
+        number_input_elements.nth(11), name="st_number_input-value_none_min_1"
+    )
+
+
+def test_number_input_has_correct_default_values(app: Page):
+    """Test that st.number_input has the correct initial values."""
+    markdown_elements = app.locator(".stMarkdown")
+    expect(markdown_elements).to_have_count(13)
+
+    expected = [
+        "number input 1 (default) - value: 0.0",
+        "number input 2 (value=1) - value: 1",
+        "number input 3 (min & max) - value: 1",
+        "number input 4 (step=2) - value: 0",
+        "number input 5 (max=10) - value: 0",
+        "number input 6 (disabled=True) - value: 0.0",
+        "number input 7 (label=hidden) - value: 0.0",
+        "number input 8 (label=collapsed) - value: 0.0",
+        "number input 9 (on_change) - value: 0.0",
+        "number input 9 (on_change) - changed: False",
+        "number input 10 (small width) - value: 0",
+        "number input 11 (value=None) - value: None",
+        "number input 12 (value from state & min=1) - value: 10",
+    ]
+
+    for markdown_element, expected_text in zip(markdown_elements.all(), expected):
+        expect(markdown_element).to_have_text(expected_text, use_inner_text=True)
+
+
+def test_number_input_shows_instructions_when_dirty(
+    app: Page, assert_snapshot: ImageCompareFunction
+):
+    """Test that st.number_input shows the instructions correctly when dirty."""
+    first_number_input_field = app.locator(".stNumberInput input").nth(0)
+    first_number_input_field.fill("10")
+
+    assert_snapshot(first_number_input_field, name="st_number_input-input_instructions")
+
+
+def test_number_input_updates_value_correctly_on_enter(app: Page):
+    """Test that st.number_input updates the value correctly on enter."""
+    first_number_input_field = app.locator(".stNumberInput input").nth(0)
+    first_number_input_field.fill("10")
+    first_number_input_field.press("Enter")
+
+    expect(app.locator(".stMarkdown").nth(0)).to_have_text(
+        "number input 1 (default) - value: 10.0", use_inner_text=True
+    )
+
+
+def test_number_input_has_correct_value_on_increment_click(app: Page):
+    """Test that st.number_input has the correct value on increment click."""
+    number_input_up_buttons = app.locator(".stNumberInput button.step-up")
+    expect(number_input_up_buttons).to_have_count(11)
+    for i, button in enumerate(number_input_up_buttons.all()):
+        if i not in [5, 9]:
+            button.click()
+
+    markdown_elements = app.locator(".stMarkdown")
+
+    expected = [
+        "number input 1 (default) - value: 0.01",
+        "number input 2 (value=1) - value: 2",
+        "number input 3 (min & max) - value: 2",
+        "number input 4 (step=2) - value: 2",
+        "number input 5 (max=10) - value: 1",
+        "number input 6 (disabled=True) - value: 0.0",
+        "number input 7 (label=hidden) - value: 0.01",
+        "number input 8 (label=collapsed) - value: 0.01",
+        "number input 9 (on_change) - value: 0.01",
+        "number input 9 (on_change) - changed: True",
+        "number input 10 (small width) - value: 0",
+        "number input 11 (value=None) - value: None",
+        "number input 12 (value from state & min=1) - value: 11",
+    ]
+
+    for markdown_element, expected_text in zip(markdown_elements.all(), expected):
+        expect(markdown_element).to_have_text(expected_text, use_inner_text=True)
+
+
+def test_number_input_has_correct_value_on_arrow_up(app: Page):
+    """Test that st.number_input has the correct value on arrow up."""
+    first_number_input_field = app.locator(".stNumberInput input").nth(0)
+    first_number_input_field.press("ArrowUp")
+
+    expect(app.locator(".stMarkdown").nth(0)).to_have_text(
+        "number input 1 (default) - value: 0.01", use_inner_text=True
+    )
+
+
+def test_number_input_has_correct_value_on_blur(app: Page):
+    """Test that st.number_input has the correct value on blur."""
+
+    first_number_input_field = app.locator(".stNumberInput input").nth(0)
+    first_number_input_field.focus()
+    first_number_input_field.fill("10")
+    first_number_input_field.blur()
+
+    expect(app.locator(".stMarkdown").nth(0)).to_have_text(
+        "number input 1 (default) - value: 10.0", use_inner_text=True
+    )
+
+
+def test_empty_number_input_behaves_correctly(
+    app: Page, assert_snapshot: ImageCompareFunction
+):
+    """Test that st.number_input behaves correctly when empty."""
+    # Enter 10 in the first empty input:
+    empty_number_input = app.locator(".stNumberInput input").nth(10)
+    empty_number_input.fill("10")
+    empty_number_input.press("Enter")
+
+    expect(app.locator(".stMarkdown").nth(11)).to_have_text(
+        "number input 11 (value=None) - value: 10.0", use_inner_text=True
+    )
+
+    assert_snapshot(empty_number_input, name="st_number_input-clearable_input")
+
+    # Press escape to clear value:
+    empty_number_input = app.locator(".stNumberInput").nth(10)
+    empty_number_input.focus()
+    empty_number_input.press("Escape")
+    empty_number_input.press("Enter")
+
+    # Should be empty again:
+    expect(app.locator(".stMarkdown").nth(11)).to_have_text(
+        "number input 11 (value=None) - value: None", use_inner_text=True
+    )
+
+    # Check with second empty input, this one should be integer since the min_value was
+    # set to an integer:
+    empty_number_input_with_min = app.locator(".stNumberInput input").nth(11)
+    empty_number_input_with_min.fill("15")
+    empty_number_input_with_min.press("Enter")
+
+    expect(app.locator(".stMarkdown").nth(12)).to_have_text(
+        "number input 12 (value from state & min=1) - value: 15", use_inner_text=True
+    )
diff --git a/frontend/lib/src/WidgetStateManager.ts b/frontend/lib/src/WidgetStateManager.ts
index dcfd184bf5c4..16e11b3b87fe 100644
--- a/frontend/lib/src/WidgetStateManager.ts
+++ b/frontend/lib/src/WidgetStateManager.ts
@@ -309,7 +309,11 @@ export class WidgetStateManager {
     return undefined
   }
 
-  public setIntValue(widget: WidgetInfo, value: number, source: Source): void {
+  public setIntValue(
+    widget: WidgetInfo,
+    value: number | null,
+    source: Source
+  ): void {
     this.createWidgetState(widget, source).intValue = value
     this.onWidgetValueChanged(widget.formId, source)
   }
@@ -325,7 +329,7 @@ export class WidgetStateManager {
 
   public setDoubleValue(
     widget: WidgetInfo,
-    value: number,
+    value: number | null,
     source: Source
   ): void {
     this.createWidgetState(widget, source).doubleValue = value
diff --git a/frontend/lib/src/components/widgets/NumberInput/NumberInput.test.tsx b/frontend/lib/src/components/widgets/NumberInput/NumberInput.test.tsx
index a88ad32292f8..26461e22c63d 100644
--- a/frontend/lib/src/components/widgets/NumberInput/NumberInput.test.tsx
+++ b/frontend/lib/src/components/widgets/NumberInput/NumberInput.test.tsx
@@ -13,29 +13,32 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
+import React from "react"
 import { ShallowWrapper } from "enzyme"
 import {
   LabelVisibilityMessage as LabelVisibilityMessageProto,
   NumberInput as NumberInputProto,
 } from "@streamlit/lib/src/proto"
-import React from "react"
+
 import { mount, shallow } from "@streamlit/lib/src/test_util"
 import { StyledInputControls } from "@streamlit/lib/src/components/widgets/NumberInput/styled-components"
 import { Input as UIInput } from "baseui/input"
 import { WidgetStateManager } from "@streamlit/lib/src/WidgetStateManager"
+import { mockTheme } from "@streamlit/lib/src/mocks/mockTheme"
 
-import NumberInput, { Props, State } from "./NumberInput"
+import { NumberInput, Props, State } from "./NumberInput"
 
 const getProps = (elementProps: Partial<NumberInputProto> = {}): Props => ({
   element: NumberInputProto.create({
     label: "Label",
+    default: 0,
     hasMin: false,
     hasMax: false,
     ...elementProps,
   }),
   width: 300,
   disabled: false,
+  theme: mockTheme.emotion,
   widgetMgr: new WidgetStateManager({
     sendRerunBackMsg: jest.fn(),
     formsDataChanged: jest.fn(),
diff --git a/frontend/lib/src/components/widgets/NumberInput/NumberInput.tsx b/frontend/lib/src/components/widgets/NumberInput/NumberInput.tsx
index 06c3098d25ee..974f30a8283b 100644
--- a/frontend/lib/src/components/widgets/NumberInput/NumberInput.tsx
+++ b/frontend/lib/src/components/widgets/NumberInput/NumberInput.tsx
@@ -16,7 +16,9 @@
 
 import React from "react"
 import { Plus, Minus } from "@emotion-icons/open-iconic"
+import { withTheme } from "@emotion/react"
 import { sprintf } from "sprintf-js"
+
 import { FormClearHelper } from "@streamlit/lib/src/components/widgets/Form"
 import { logWarning } from "@streamlit/lib/src/util/log"
 import { NumberInput as NumberInputProto } from "@streamlit/lib/src/proto"
@@ -27,7 +29,6 @@ import {
 } from "@streamlit/lib/src/WidgetStateManager"
 import TooltipIcon from "@streamlit/lib/src/components/shared/TooltipIcon"
 import { Placement } from "@streamlit/lib/src/components/shared/Tooltip"
-
 import Icon from "@streamlit/lib/src/components/shared/Icon"
 import { Input as UIInput } from "baseui/input"
 import InputInstructions from "@streamlit/lib/src/components/shared/InputInstructions/InputInstructions"
@@ -35,10 +36,12 @@ import {
   WidgetLabel,
   StyledWidgetLabelHelp,
 } from "@streamlit/lib/src/components/widgets/BaseWidget"
-
+import { EmotionTheme } from "@streamlit/lib/src/theme"
 import {
   isInForm,
   labelVisibilityProtoValueToEnum,
+  isNullOrUndefined,
+  notNullOrUndefined,
 } from "@streamlit/lib/src/util/utils"
 
 import {
@@ -53,6 +56,7 @@ export interface Props {
   element: NumberInputProto
   widgetMgr: WidgetStateManager
   width: number
+  theme: EmotionTheme
 }
 
 export interface State {
@@ -65,12 +69,12 @@ export interface State {
    * The value specified by the user via the UI. If the user didn't touch this
    * widget's UI, the default value is used.
    */
-  value: number
+  value: number | null
 
   /**
    * The value with applied format that is going to be shown to the user
    */
-  formattedValue: string
+  formattedValue: string | null
 
   /**
    * True if the input is selected
@@ -78,7 +82,7 @@ export interface State {
   isFocused: boolean
 }
 
-class NumberInput extends React.PureComponent<Props, State> {
+export class NumberInput extends React.PureComponent<Props, State> {
   private readonly formClearHelper = new FormClearHelper()
 
   private inputRef = React.createRef<HTMLInputElement | HTMLTextAreaElement>()
@@ -94,14 +98,14 @@ class NumberInput extends React.PureComponent<Props, State> {
     }
   }
 
-  get initialValue(): number {
+  get initialValue(): number | null {
     // If WidgetStateManager knew a value for this widget, initialize to that.
     // Otherwise, use the default value from the widget protobuf
     const storedValue = this.isIntData()
       ? this.props.widgetMgr.getIntValue(this.props.element)
       : this.props.widgetMgr.getDoubleValue(this.props.element)
 
-    return storedValue !== undefined ? storedValue : this.props.element.default
+    return storedValue ?? this.props.element.default ?? null
   }
 
   public componentDidMount(): void {
@@ -130,12 +134,22 @@ class NumberInput extends React.PureComponent<Props, State> {
   private updateFromProtobuf(): void {
     const { value } = this.props.element
     this.props.element.setValue = false
-    this.setState({ value, formattedValue: this.formatValue(value) }, () => {
-      this.commitWidgetValue({ fromUi: false })
-    })
+    this.setState(
+      {
+        value: value ?? null,
+        formattedValue: this.formatValue(value ?? null),
+      },
+      () => {
+        this.commitWidgetValue({ fromUi: false })
+      }
+    )
   }
 
-  private formatValue = (value: number): string => {
+  private formatValue = (value: number | null): string | null => {
+    if (isNullOrUndefined(value)) {
+      return null
+    }
+
     const format = getNonEmptyString(this.props.element.format)
     if (format == null) {
       return value.toString()
@@ -183,13 +197,13 @@ class NumberInput extends React.PureComponent<Props, State> {
     const min = this.getMin()
     const max = this.getMax()
 
-    if (min > value || value > max) {
+    if (notNullOrUndefined(value) && (min > value || value > max)) {
       const node = this.inputRef.current
       if (node) {
         node.reportValidity()
       }
     } else {
-      const valueToBeSaved = value || value === 0 ? value : data.default
+      const valueToBeSaved = value ?? data.default ?? null
 
       if (this.isIntData()) {
         widgetMgr.setIntValue(element, valueToBeSaved, source)
@@ -212,7 +226,7 @@ class NumberInput extends React.PureComponent<Props, State> {
   private onFormCleared = (): void => {
     this.setState(
       (_, prevProps) => {
-        return { value: prevProps.element.default }
+        return { value: prevProps.element.default ?? null }
       },
       () => this.commitWidgetValue({ fromUi: true })
     )
@@ -235,19 +249,27 @@ class NumberInput extends React.PureComponent<Props, State> {
   ): void => {
     const { value } = e.target
 
-    let numValue: number
-
-    if (this.isIntData()) {
-      numValue = parseInt(value, 10)
+    if (value === "") {
+      this.setState({
+        dirty: true,
+        value: null,
+        formattedValue: null,
+      })
     } else {
-      numValue = parseFloat(value)
-    }
+      let numValue: number
 
-    this.setState({
-      dirty: true,
-      value: numValue,
-      formattedValue: value,
-    })
+      if (this.isIntData()) {
+        numValue = parseInt(value, 10)
+      } else {
+        numValue = parseFloat(value)
+      }
+
+      this.setState({
+        dirty: true,
+        value: numValue,
+        formattedValue: value,
+      })
+    }
   }
 
   private onKeyDown = (
@@ -285,11 +307,19 @@ class NumberInput extends React.PureComponent<Props, State> {
 
   /** True if the input's current value can be decremented by its step. */
   private get canDecrement(): boolean {
+    if (isNullOrUndefined(this.state.value)) {
+      return false
+    }
+
     return this.state.value - this.getStep() >= this.getMin()
   }
 
   /** True if the input's current value can be incremented by its step. */
   private get canIncrement(): boolean {
+    if (isNullOrUndefined(this.state.value)) {
+      return false
+    }
+
     return this.state.value + this.getStep() <= this.getMax()
   }
 
@@ -305,7 +335,7 @@ class NumberInput extends React.PureComponent<Props, State> {
             this.setState(
               {
                 dirty: true,
-                value: value + step,
+                value: (value ?? this.getMin()) + step,
               },
               () => {
                 this.commitWidgetValue({ fromUi: true })
@@ -318,7 +348,7 @@ class NumberInput extends React.PureComponent<Props, State> {
             this.setState(
               {
                 dirty: true,
-                value: value - step,
+                value: (value ?? this.getMax()) - step,
               },
               () => {
                 this.commitWidgetValue({ fromUi: true })
@@ -331,13 +361,14 @@ class NumberInput extends React.PureComponent<Props, State> {
     }
 
   public render(): React.ReactNode {
-    const { element, width, disabled, widgetMgr } = this.props
+    const { element, width, disabled, widgetMgr, theme } = this.props
     const { formattedValue, dirty, isFocused } = this.state
 
     const style = { width }
 
     const disableDecrement = !this.canDecrement || disabled
     const disableIncrement = !this.canIncrement || disabled
+    const clearable = isNullOrUndefined(element.default) && !disabled
 
     // Manage our form-clear event handler.
     this.formClearHelper.manageFormClearListener(
@@ -347,7 +378,7 @@ class NumberInput extends React.PureComponent<Props, State> {
     )
 
     return (
-      <div className="stNumberInput" style={style}>
+      <div className="stNumberInput" style={style} data-testid="stNumberInput">
         <WidgetLabel
           label={element.label}
           disabled={disabled}
@@ -368,15 +399,38 @@ class NumberInput extends React.PureComponent<Props, State> {
           <UIInput
             type="number"
             inputRef={this.inputRef}
-            value={formattedValue}
+            value={formattedValue || undefined}
             onBlur={this.onBlur}
             onFocus={this.onFocus}
             onChange={this.onChange}
             onKeyPress={this.onKeyPress}
             onKeyDown={this.onKeyDown}
+            clearable={clearable}
+            clearOnEscape={clearable}
             disabled={disabled}
             aria-label={element.label}
             overrides={{
+              ClearIcon: {
+                props: {
+                  overrides: {
+                    Svg: {
+                      style: {
+                        color: theme.colors.darkGray,
+                        // Since the close icon is an SVG, and we can't control its viewbox nor its attributes,
+                        // Let's use a scale transform effect to make it bigger.
+                        // The width property only enlarges its bounding box, so it's easier to click.
+                        transform: "scale(1.20)",
+                        width: theme.spacing.twoXL,
+                        marginRight: "-1.3em",
+
+                        ":hover": {
+                          fill: theme.colors.bodyText,
+                        },
+                      },
+                    },
+                  },
+                },
+              },
               Input: {
                 props: {
                   step: this.getStep(),
@@ -445,7 +499,7 @@ class NumberInput extends React.PureComponent<Props, State> {
         <StyledInstructionsContainer>
           <InputInstructions
             dirty={dirty}
-            value={formattedValue}
+            value={formattedValue ?? ""}
             className="input-instructions"
             inForm={isInForm({ formId: element.formId })}
           />
@@ -465,4 +519,4 @@ function getNonEmptyString(
   return value == null || value === "" ? undefined : value
 }
 
-export default NumberInput
+export default withTheme(NumberInput)
diff --git a/lib/streamlit/elements/widgets/number_input.py b/lib/streamlit/elements/widgets/number_input.py
index 8d7db13ad45b..b3e072c70104 100644
--- a/lib/streamlit/elements/widgets/number_input.py
+++ b/lib/streamlit/elements/widgets/number_input.py
@@ -12,10 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from __future__ import annotations
+
 import numbers
 from dataclasses import dataclass
 from textwrap import dedent
-from typing import Optional, Union, cast
+from typing import Union, cast, overload
 
 import streamlit
 from streamlit.elements.form import current_form_id
@@ -30,7 +32,7 @@
 from streamlit.runtime.metrics_util import gather_metrics
 from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
 from streamlit.runtime.state import (
-    NoValue,
+    DefaultValue,
     WidgetArgs,
     WidgetCallback,
     WidgetKwargs,
@@ -44,44 +46,82 @@
 
 @dataclass
 class NumberInputSerde:
-    value: Union[int, float]
+    value: Number | None
     data_type: int
 
-    def serialize(self, v: Number) -> Number:
+    def serialize(self, v: Number | None) -> Number | None:
         return v
 
-    def deserialize(self, ui_value: Optional[Number], widget_id: str = "") -> Number:
-        if ui_value is not None:
-            val = ui_value
-        else:
-            # Widget has not been used; fallback to the original value,
-            val = self.value
+    def deserialize(
+        self, ui_value: Number | None, widget_id: str = ""
+    ) -> Number | None:
+        val: Number | None = ui_value if ui_value is not None else self.value
 
-        if self.data_type == NumberInputProto.INT:
+        if val is not None and self.data_type == NumberInputProto.INT:
             val = int(val)
 
         return val
 
 
 class NumberInputMixin:
-    @gather_metrics("number_input")
+    @overload
     def number_input(
         self,
         label: str,
-        min_value: Optional[Number] = None,
-        max_value: Optional[Number] = None,
-        value: Union[NoValue, Number, None] = NoValue(),
-        step: Optional[Number] = None,
-        format: Optional[str] = None,
-        key: Optional[Key] = None,
-        help: Optional[str] = None,
-        on_change: Optional[WidgetCallback] = None,
-        args: Optional[WidgetArgs] = None,
-        kwargs: Optional[WidgetKwargs] = None,
+        min_value: Number | None = None,
+        max_value: Number | None = None,
+        value: DefaultValue | Number = DefaultValue(),
+        step: Number | None = None,
+        format: str | None = None,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
         *,  # keyword-only arguments:
         disabled: bool = False,
         label_visibility: LabelVisibility = "visible",
     ) -> Number:
+        pass
+
+    @overload
+    def number_input(
+        self,
+        label: str,
+        min_value: Number | None = None,
+        max_value: Number | None = None,
+        value: None = None,
+        step: Number | None = None,
+        format: str | None = None,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
+        *,  # keyword-only arguments:
+        disabled: bool = False,
+        label_visibility: LabelVisibility = "visible",
+    ) -> Number | None:
+        pass
+
+    @gather_metrics("number_input")
+    def number_input(
+        self,
+        label: str,
+        min_value: Number | None = None,
+        max_value: Number | None = None,
+        value: DefaultValue | Number | None = DefaultValue(),
+        step: Number | None = None,
+        format: str | None = None,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
+        *,  # keyword-only arguments:
+        disabled: bool = False,
+        label_visibility: LabelVisibility = "visible",
+    ) -> Number | None:
         r"""Display a numeric input widget.
 
         .. note::
@@ -125,8 +165,9 @@ def number_input(
             The maximum permitted value.
             If None, there will be no maximum.
         value : int, float, or None
-            The value of this widget when it first renders.
-            Defaults to min_value, or 0.0 if min_value is None
+            The value of this widget when it first renders. If ``None``, the widget
+            will be  initialized as empty and returns ``None`` as long as the viewer
+            didn't provide input. Defaults to min_value, or 0.0 if min_value is None.
         step : int, float, or None
             The stepping interval.
             Defaults to 1 if the value is an int, 0.01 otherwise.
@@ -197,25 +238,25 @@ def number_input(
     def _number_input(
         self,
         label: str,
-        min_value: Optional[Number] = None,
-        max_value: Optional[Number] = None,
-        value: Union[NoValue, Number, None] = NoValue(),
-        step: Optional[Number] = None,
-        format: Optional[str] = None,
-        key: Optional[Key] = None,
-        help: Optional[str] = None,
-        on_change: Optional[WidgetCallback] = None,
-        args: Optional[WidgetArgs] = None,
-        kwargs: Optional[WidgetKwargs] = None,
+        min_value: Number | None = None,
+        max_value: Number | None = None,
+        value: DefaultValue | Number | None = DefaultValue(),
+        step: Number | None = None,
+        format: str | None = None,
+        key: Key | None = None,
+        help: str | None = None,
+        on_change: WidgetCallback | None = None,
+        args: WidgetArgs | None = None,
+        kwargs: WidgetKwargs | None = None,
         *,  # keyword-only arguments:
         disabled: bool = False,
         label_visibility: LabelVisibility = "visible",
-        ctx: Optional[ScriptRunContext] = None,
-    ) -> Number:
+        ctx: ScriptRunContext | None = None,
+    ) -> Number | None:
         key = to_key(key)
         check_callback_rules(self.dg, on_change)
         check_session_state_rules(
-            default_value=None if isinstance(value, NoValue) else value, key=key
+            default_value=None if isinstance(value, DefaultValue) else value, key=key
         )
         maybe_raise_label_warnings(label, label_visibility)
 
@@ -237,12 +278,12 @@ def _number_input(
         number_input_args = [min_value, max_value, value, step]
 
         int_args = all(
-            isinstance(a, (numbers.Integral, type(None), NoValue))
+            isinstance(a, (numbers.Integral, type(None), DefaultValue))
             for a in number_input_args
         )
 
         float_args = all(
-            isinstance(a, (float, type(None), NoValue)) for a in number_input_args
+            isinstance(a, (float, type(None), DefaultValue)) for a in number_input_args
         )
 
         if not int_args and not float_args:
@@ -254,7 +295,7 @@ def _number_input(
                 f"\n`step` has {type(step).__name__} type."
             )
 
-        if isinstance(value, NoValue):
+        if isinstance(value, DefaultValue):
             if min_value is not None:
                 value = min_value
             elif int_args and float_args:
@@ -268,28 +309,31 @@ def _number_input(
         float_value = isinstance(value, float)
 
         if value is None:
-            raise StreamlitAPIException(
-                "Default value for number_input should be an int or a float."
+            if int_args and not float_args:
+                # Select int type if all relevant args are ints:
+                int_value = True
+            else:
+                # Otherwise, defaults to float:
+                float_value = True
+
+        if format is None:
+            format = "%d" if int_value else "%0.2f"
+
+        # Warn user if they format an int type as a float or vice versa.
+        if format in ["%d", "%u", "%i"] and float_value:
+            import streamlit as st
+
+            st.warning(
+                "Warning: NumberInput value below has type float,"
+                f" but format {format} displays as integer."
+            )
+        elif format[-1] == "f" and int_value:
+            import streamlit as st
+
+            st.warning(
+                "Warning: NumberInput value below has type int so is"
+                f" displayed as int despite format string {format}."
             )
-        else:
-            if format is None:
-                format = "%d" if int_value else "%0.2f"
-
-            # Warn user if they format an int type as a float or vice versa.
-            if format in ["%d", "%u", "%i"] and float_value:
-                import streamlit as st
-
-                st.warning(
-                    "Warning: NumberInput value below has type float,"
-                    f" but format {format} displays as integer."
-                )
-            elif format[-1] == "f" and int_value:
-                import streamlit as st
-
-                st.warning(
-                    "Warning: NumberInput value below has type int so is"
-                    f" displayed as int despite format string {format}."
-                )
 
         if step is None:
             step = 1 if int_value else 0.01
@@ -309,11 +353,10 @@ def _number_input(
             raise StreamlitAPIException(
                 f"The default `value` {value} must be greater than or equal to the `min_value` {min_value}"
             )
-        if max_value is not None and value is not None:
-            if max_value < value:
-                raise StreamlitAPIException(
-                    f"The default `value` {value} must be less than or equal to the `max_value` {max_value}"
-                )
+        if max_value is not None and value is not None and max_value < value:
+            raise StreamlitAPIException(
+                f"The default `value` {value} must be less than or equal to the `max_value` {max_value}"
+            )
 
         # Bounds checks. JSNumber produces human-readable exceptions that
         # we simply re-package as StreamlitAPIExceptions.
@@ -329,7 +372,8 @@ def _number_input(
                     )
                 if step is not None:
                     JSNumber.validate_int_bounds(step, "`step`")  # type: ignore
-                JSNumber.validate_int_bounds(value, "`value`")  # type: ignore
+                if value is not None:
+                    JSNumber.validate_int_bounds(value, "`value`")  # type: ignore
             else:
                 if min_value is not None:
                     JSNumber.validate_float_bounds(min_value, "`min_value`")
@@ -337,7 +381,8 @@ def _number_input(
                     JSNumber.validate_float_bounds(max_value, "`max_value`")
                 if step is not None:
                     JSNumber.validate_float_bounds(step, "`step`")
-                JSNumber.validate_float_bounds(value, "`value`")
+                if value is not None:
+                    JSNumber.validate_float_bounds(value, "`value`")
         except JSNumberBoundsException as e:
             raise StreamlitAPIException(str(e))
 
@@ -347,7 +392,8 @@ def _number_input(
         number_input_proto.id = id
         number_input_proto.data_type = data_type
         number_input_proto.label = label
-        number_input_proto.default = value
+        if value is not None:
+            number_input_proto.default = value
         number_input_proto.form_id = current_form_id(self.dg)
         number_input_proto.disabled = disabled
         number_input_proto.label_visibility.value = get_label_visibility_proto_value(
@@ -385,7 +431,8 @@ def _number_input(
         )
 
         if widget_state.value_changed:
-            number_input_proto.value = widget_state.value
+            if widget_state.value is not None:
+                number_input_proto.value = widget_state.value
             number_input_proto.set_value = True
 
         self.dg._enqueue("number_input", number_input_proto)
diff --git a/lib/streamlit/runtime/state/__init__.py b/lib/streamlit/runtime/state/__init__.py
index 8a86e9168299..109704084f71 100644
--- a/lib/streamlit/runtime/state/__init__.py
+++ b/lib/streamlit/runtime/state/__init__.py
@@ -33,6 +33,7 @@
 from streamlit.runtime.state.session_state_proxy import (
     get_session_state as get_session_state,
 )
+from streamlit.runtime.state.widgets import DefaultValue as DefaultValue
 from streamlit.runtime.state.widgets import NoValue as NoValue
 from streamlit.runtime.state.widgets import (
     coalesce_widget_states as coalesce_widget_states,
diff --git a/lib/streamlit/runtime/state/common.py b/lib/streamlit/runtime/state/common.py
index 57f541b943ca..ff6d725fa82d 100644
--- a/lib/streamlit/runtime/state/common.py
+++ b/lib/streamlit/runtime/state/common.py
@@ -57,7 +57,7 @@
 from streamlit.type_util import ValueFieldName
 
 if TYPE_CHECKING:
-    from streamlit.runtime.state.widgets import NoValue
+    from streamlit.runtime.state.widgets import DefaultValue, NoValue
 
 
 # Protobuf types for all widgets.
@@ -157,7 +157,15 @@ def failure(
 
 PROTO_SCALAR_VALUE = Union[float, int, bool, str, bytes]
 SAFE_VALUES = Union[
-    date, time, datetime, timedelta, None, "NoValue", Message, PROTO_SCALAR_VALUE
+    date,
+    time,
+    datetime,
+    timedelta,
+    None,
+    "NoValue",
+    "DefaultValue",
+    Message,
+    PROTO_SCALAR_VALUE,
 ]
 
 
diff --git a/lib/streamlit/runtime/state/session_state.py b/lib/streamlit/runtime/state/session_state.py
index d0e6f056f3de..07475c483f46 100644
--- a/lib/streamlit/runtime/state/session_state.py
+++ b/lib/streamlit/runtime/state/session_state.py
@@ -112,7 +112,11 @@ def __getitem__(self, k: str) -> Any:
             ValueFieldName,
             wstate.value.WhichOneof("value"),
         )
-        value = wstate.value.__getattribute__(value_field_name)
+        value = (
+            wstate.value.__getattribute__(value_field_name)
+            if value_field_name  # Field name is None if the widget value was cleared
+            else None
+        )
 
         if is_array_value_field_name(value_field_name):
             # Array types are messages with data in a `data` field
@@ -210,6 +214,7 @@ def get_serialized(self, k: str) -> WidgetStateProto | None:
 
         field = metadata.value_type
         serialized = metadata.serializer(item.value)
+
         if is_array_value_field_name(field):
             arr = getattr(widget, field)
             arr.data.extend(serialized)
@@ -219,7 +224,11 @@ def get_serialized(self, k: str) -> WidgetStateProto | None:
             widget.file_uploader_state_value.CopyFrom(serialized)
         elif field == "string_trigger_value":
             widget.string_trigger_value.CopyFrom(serialized)
-        else:
+        elif field is not None and serialized is not None:
+            # If the field is None, the widget value was cleared
+            # by the user and therefore is None. But we cannot
+            # set it to None here, since the proto properties are
+            # not nullable. So we just don't set it.
             setattr(widget, field, serialized)
 
         return widget
diff --git a/lib/streamlit/runtime/state/widgets.py b/lib/streamlit/runtime/state/widgets.py
index cf305bbf5b1f..30255bda4f1c 100644
--- a/lib/streamlit/runtime/state/widgets.py
+++ b/lib/streamlit/runtime/state/widgets.py
@@ -72,6 +72,15 @@
 )
 
 
+class DefaultValue:
+    """Used as default value for element parameters to indicate that the user
+    didn't explicitly specify a value. This usually means that the user wants
+    to use a default value.
+    """
+
+    pass
+
+
 class NoValue:
     """Return this from DeltaGenerator.foo_widget() when you want the st.foo_widget()
     call to return None. This is needed because `DeltaGenerator._enqueue`
diff --git a/lib/tests/streamlit/elements/number_input_test.py b/lib/tests/streamlit/elements/number_input_test.py
index 7987117d5af7..0b6ebcf7f21b 100644
--- a/lib/tests/streamlit/elements/number_input_test.py
+++ b/lib/tests/streamlit/elements/number_input_test.py
@@ -58,6 +58,7 @@ def test_just_label(self):
             LabelVisibilityMessage.LabelVisibilityOptions.VISIBLE,
         )
         self.assertEqual(c.default, 0.0)
+        self.assertEqual(c.HasField("default"), True)
         self.assertEqual(c.has_min, False)
         self.assertEqual(c.has_max, False)
         self.assertEqual(c.disabled, False)
@@ -69,6 +70,32 @@ def test_just_disabled(self):
         c = self.get_delta_from_queue().new_element.number_input
         self.assertEqual(c.disabled, True)
 
+    def test_none_value(self):
+        """Test that it can be called with None as value."""
+        st.number_input("the label", value=None)
+
+        c = self.get_delta_from_queue().new_element.number_input
+        self.assertEqual(c.label, "the label")
+        # If a proto property is null is not determined by this value,
+        # but by the check via the HasField method:
+        self.assertEqual(c.default, 0.0)
+        self.assertEqual(c.HasField("default"), False)
+
+    def test_none_value_with_int_min(self):
+        """Test that it can be called with None as value and
+        will be interpreted as integer if min_value is set to int."""
+        st.number_input("the label", value=None, min_value=1)
+
+        c = self.get_delta_from_queue().new_element.number_input
+        self.assertEqual(c.label, "the label")
+        # If a proto property is null is not determined by this value,
+        # but by the check via the HasField method:
+        self.assertEqual(c.default, 0.0)
+        self.assertEqual(c.HasField("default"), False)
+        self.assertEqual(c.has_min, True)
+        self.assertEqual(c.min, 1)
+        self.assertEqual(c.data_type, NumberInput.INT)
+
     def test_default_value_when_min_is_passed(self):
         st.number_input("the label", min_value=1, max_value=10)
 
diff --git a/proto/streamlit/proto/NumberInput.proto b/proto/streamlit/proto/NumberInput.proto
index 7e4c86720245..309e5646690d 100644
--- a/proto/streamlit/proto/NumberInput.proto
+++ b/proto/streamlit/proto/NumberInput.proto
@@ -35,12 +35,12 @@ message NumberInput {
   bool has_max = 12;
 
   DataType data_type = 13;
-  double default = 14;
+  optional double default = 14;
   double step = 15;
   double min = 16;
   double max = 17;
   string help = 18;
-  double value = 19;
+  optional double value = 19;
   bool set_value = 20;
   bool disabled = 21;
   LabelVisibilityMessage label_visibility = 22;


Code review comments:
diff_hunk: @@ -72,6 +72,15 @@
 )
 
 
+class DefaultValue:
path: lib/streamlit/runtime/state/widgets.py
text: @user1:
If we make this a dataclass, we can simplify some `isinstance` checks to `==` equality checks, which seems more semantically fitting.

@author:
Oh, good point. I also just checked the spec and it specified to use ellipsis here instead of a class. I will check both options, change to dataclass or change to ellipsis tomorrow.