Repository: pydantic/pydantic

Issue:
`ByteSize.human_readable` with an optional separator (e.g. whitespace) between value and unit
### Initial Checks

- [X] I have searched Google & GitHub for similar requests and couldn't find anything
- [X] I have read and followed [the docs](https://docs.pydantic.dev) and still think this feature is missing

### Description

Currently, the return value of `ByteSize.human_readable` does not have any separator between the value and unit.

```python
>>> size = ByteSize(12345678)
>>> size.human_readable()
'11.8MiB'
```

It looks a little bit weird to me, and I think it could be better if there's a space separating the value and unit. Though there are many ways to handle this need, it may be a better idea to provide an optional argument with this method? For example:

```python
ByteSize.human_readable(decimal: bool = False, separator: bool = False) -> str
```

When I switch the `separator` on:

```python
>>> size = ByteSize(12345678)
>>> size.human_readable(separator=True)
'11.8 MiB'
```

It's probably easy to be implemented, so I can create a PR to implement this feature if it will be acceptable.

What do you think about it?

### Affected Components

- [ ] [Compatibility between releases](https://docs.pydantic.dev/changelog/)
- [ ] [Data validation/parsing](https://docs.pydantic.dev/concepts/models/#basic-model-usage)
- [ ] [Data serialization](https://docs.pydantic.dev/concepts/serialization/) - `.model_dump()` and `.model_dump_json()`
- [ ] [JSON Schema](https://docs.pydantic.dev/concepts/json_schema/)
- [ ] [Dataclasses](https://docs.pydantic.dev/concepts/dataclasses/)
- [ ] [Model Config](https://docs.pydantic.dev/concepts/config/)
- [X] [Field Types](https://docs.pydantic.dev/api/types/) - adding or changing a particular data type
- [ ] [Function validation decorator](https://docs.pydantic.dev/concepts/validation_decorator/)
- [ ] [Generic Models](https://docs.pydantic.dev/concepts/models/#generic-models)
- [ ] [Other Model behaviour](https://docs.pydantic.dev/concepts/models/) - `model_construct()`, pickling, private attributes, ORM mode
- [ ] [Plugins](https://docs.pydantic.dev/) and integration with other tools - mypy, FastAPI, python-devtools, Hypothesis, VS Code, PyCharm, etc.


Discussion:
@jks15satoshi,

Go for it! Just makes sure that by default, the separator is `''` so that this isn't a breaking change. Thanks for the suggestion. Ping me when you need a review :).

Resolving patch:
diff --git a/pydantic/types.py b/pydantic/types.py
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -1740,6 +1740,8 @@ class MyModel(BaseModel):
     #> 44.4PiB
     print(m.size.human_readable(decimal=True))
     #> 50.0PB
+    print(m.size.human_readable(separator=' '))
+    #> 44.4 PiB
 
     print(m.size.to('TiB'))
     #> 45474.73508864641
@@ -1818,12 +1820,13 @@ def _validate(cls, __input_value: Any, _: core_schema.ValidationInfo) -> ByteSiz
 
         return cls(int(float(scalar) * unit_mult))
 
-    def human_readable(self, decimal: bool = False) -> str:
+    def human_readable(self, decimal: bool = False, separator: str = '') -> str:
         """Converts a byte size to a human readable string.
 
         Args:
             decimal: If True, use decimal units (e.g. 1000 bytes per KB). If False, use binary units
                 (e.g. 1024 bytes per KiB).
+            separator: A string used to split the value and unit. Defaults to an empty string ('').
 
         Returns:
             A human readable string representation of the byte size.
@@ -1841,12 +1844,12 @@ def human_readable(self, decimal: bool = False) -> str:
         for unit in units:
             if abs(num) < divisor:
                 if unit == 'B':
-                    return f'{num:0.0f}{unit}'
+                    return f'{num:0.0f}{separator}{unit}'
                 else:
-                    return f'{num:0.1f}{unit}'
+                    return f'{num:0.1f}{separator}{unit}'
             num /= divisor
 
-        return f'{num:0.1f}{final_unit}'
+        return f'{num:0.1f}{separator}{final_unit}'
 
     def to(self, unit: str) -> float:
         """Converts a byte size to another unit, including both byte and bit units.


Test patch:
diff --git a/tests/test_types.py b/tests/test_types.py
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -4436,23 +4436,23 @@ class FrozenSetModel(BaseModel):
 
 
 @pytest.mark.parametrize(
-    'input_value,output,human_bin,human_dec',
+    'input_value,output,human_bin,human_dec,human_sep',
     (
-        (1, 1, '1B', '1B'),
-        ('1', 1, '1B', '1B'),
-        ('1.0', 1, '1B', '1B'),
-        ('1b', 1, '1B', '1B'),
-        ('1.5 KB', int(1.5e3), '1.5KiB', '1.5KB'),
-        ('1.5 K', int(1.5e3), '1.5KiB', '1.5KB'),
-        ('1.5 MB', int(1.5e6), '1.4MiB', '1.5MB'),
-        ('1.5 M', int(1.5e6), '1.4MiB', '1.5MB'),
-        ('5.1kib', 5222, '5.1KiB', '5.2KB'),
-        ('6.2EiB', 7148113328562451456, '6.2EiB', '7.1EB'),
-        ('8bit', 1, '1B', '1B'),
-        ('1kbit', 125, '125B', '125B'),
+        (1, 1, '1B', '1B', '1 B'),
+        ('1', 1, '1B', '1B', '1 B'),
+        ('1.0', 1, '1B', '1B', '1 B'),
+        ('1b', 1, '1B', '1B', '1 B'),
+        ('1.5 KB', int(1.5e3), '1.5KiB', '1.5KB', '1.5 KiB'),
+        ('1.5 K', int(1.5e3), '1.5KiB', '1.5KB', '1.5 KiB'),
+        ('1.5 MB', int(1.5e6), '1.4MiB', '1.5MB', '1.4 MiB'),
+        ('1.5 M', int(1.5e6), '1.4MiB', '1.5MB', '1.4 MiB'),
+        ('5.1kib', 5222, '5.1KiB', '5.2KB', '5.1 KiB'),
+        ('6.2EiB', 7148113328562451456, '6.2EiB', '7.1EB', '6.2 EiB'),
+        ('8bit', 1, '1B', '1B', '1 B'),
+        ('1kbit', 125, '125B', '125B', '125 B'),
     ),
 )
-def test_bytesize_conversions(input_value, output, human_bin, human_dec):
+def test_bytesize_conversions(input_value, output, human_bin, human_dec, human_sep):
     class Model(BaseModel):
         size: ByteSize
 
@@ -4462,6 +4462,7 @@ class Model(BaseModel):
 
     assert m.size.human_readable() == human_bin
     assert m.size.human_readable(decimal=True) == human_dec
+    assert m.size.human_readable(separator=' ') == human_sep
 
 
 def test_bytesize_to():