Repository: pandas-dev/pandas (python)

Issue:
BUG: Dataframe.to_excel treats decimal.Decimal as string instead of numeric type, the data in the Excel cell is formatted as a string, not a number
### Pandas version checks

- [X] I have checked that this issue has not already been reported.

- [X] I have confirmed this bug exists on the [latest version](https://pandas.pydata.org/docs/whatsnew/index.html) of pandas.

- [X] I have confirmed this bug exists on the main branch of pandas.


### Reproducible Example

```python
# ----------------------------------
### imports

import pandas as pd
import decimal as dc

# ----------------------------------
### definitions

value_str = '2.445'

# ----------------------------------
### initialization

df_float = pd.DataFrame(data=[float(value_str)])
df_decimal = pd.DataFrame(data=[dc.Decimal(value_str)])

# ----------------------------------
### exports

# excel
df_float.to_excel('float.xlsx')
df_decimal.to_excel('decimal.xlsx')
```


### Issue Description

The code writes value to float.xlsx as number, but it is written as string in decimal.xlsx.

I identified that in the condition here https://github.com/pandas-dev/pandas/blob/d56f6e2bca75f10bc490aa320c4e1bc295e367bd/pandas/io/excel/_base.py#L1367 decimal.Decimal datatype is not treated, and it is converted to string. I'd propose to return decimal.Decimal without converting to string, but it will require testing if excel writing libraries can handle it.

This same issue was already raised, but it was closed with no solution (by error?) https://github.com/pandas-dev/pandas/issues/26277

### Expected Behavior

Values in both dataframes should be written to excel as number.

### Installed Versions

<details>

INSTALLED VERSIONS
------------------
commit           : 91111fd99898d9dcaa6bf6bedb662db4108da6e6
python           : 3.9.13.final.0
python-bits      : 64
OS               : Linux
OS-release       : 6.0.5-200.fc36.x86_64
Version          : #1 SMP PREEMPT_DYNAMIC Wed Oct 26 15:55:21 UTC 2022
machine          : x86_64
processor        : x86_64
byteorder        : little
LC_ALL           : None
LANG             : en_GB.UTF-8
LOCALE           : en_GB.UTF-8

pandas           : 1.5.1
numpy            : 1.23.4
pytz             : 2022.6
dateutil         : 2.8.2
setuptools       : 65.5.1
pip              : 22.3.1
Cython           : None
pytest           : None
hypothesis       : None
sphinx           : None
blosc            : None
feather          : None
xlsxwriter       : None
lxml.etree       : None
html5lib         : None
pymysql          : None
psycopg2         : None
jinja2           : None
IPython          : None
pandas_datareader: None
bs4              : None
bottleneck       : None
brotli           : None
fastparquet      : None
fsspec           : None
gcsfs            : None
matplotlib       : None
numba            : None
numexpr          : None
odfpy            : None
openpyxl         : 3.0.10
pandas_gbq       : None
pyarrow          : None
pyreadstat       : None
pyxlsb           : None
s3fs             : None
scipy            : None
snappy           : None
sqlalchemy       : None
tables           : None
tabulate         : None
xarray           : None
xlrd             : None
xlwt             : None
zstandard        : None
tzdata           : None

</details>


Pull request:
BUG: Fix to_excel storing decimals as strings instead of numbers (issue #49598)
- [x] closes #49598
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [x] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [x] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.

This is the same bug fix code as PR #50174, which went stale and had no tests. The author of that PR is no longer responding. This PR adds tests and release notes.

Patch:
diff --git a/doc/source/whatsnew/v2.3.0.rst b/doc/source/whatsnew/v2.3.0.rst
index 922cc0ead7..d57d86f4a1 100644
--- a/doc/source/whatsnew/v2.3.0.rst
+++ b/doc/source/whatsnew/v2.3.0.rst
@@ -133,7 +133,7 @@ MultiIndex
 
 I/O
 ^^^
--
+- :meth:`DataFrame.to_excel` was storing decimals as strings instead of numbers (:issue:`49598`)
 -
 
 Period
diff --git a/pandas/io/excel/_base.py b/pandas/io/excel/_base.py
index ef52107c28..ced2ad91db 100644
--- a/pandas/io/excel/_base.py
+++ b/pandas/io/excel/_base.py
@@ -8,6 +8,7 @@ from collections.abc import (
     Sequence,
 )
 import datetime
+from decimal import Decimal
 from functools import partial
 import os
 from textwrap import fill
@@ -43,6 +44,7 @@ from pandas.util._validators import check_dtype_backend
 
 from pandas.core.dtypes.common import (
     is_bool,
+    is_decimal,
     is_file_like,
     is_float,
     is_integer,
@@ -1348,6 +1350,8 @@ class ExcelWriter(Generic[_WorkbookT]):
             val = float(val)
         elif is_bool(val):
             val = bool(val)
+        elif is_decimal(val):
+            val = Decimal(val)
         elif isinstance(val, datetime.datetime):
             fmt = self._datetime_format
         elif isinstance(val, datetime.date):


Test patch:
diff --git a/pandas/tests/io/excel/test_writers.py b/pandas/tests/io/excel/test_writers.py
index 44266ae9a6..81aa0be24b 100644
--- a/pandas/tests/io/excel/test_writers.py
+++ b/pandas/tests/io/excel/test_writers.py
@@ -3,6 +3,7 @@ from datetime import (
     datetime,
     timedelta,
 )
+from decimal import Decimal
 from functools import partial
 from io import BytesIO
 import os
@@ -977,6 +978,36 @@ class TestExcelWriter:
         )
         tm.assert_frame_equal(result, expected)
 
+    def test_to_excel_datatypes_preserved(self, tmp_excel):
+        # Test that when writing and reading Excel with dtype=object,
+        # datatypes are preserved, except Decimals which should be
+        # stored as floats
+
+        # see gh-49598
+        df = DataFrame(
+            [
+                [1.23, "1.23", Decimal("1.23")],
+                [4.56, "4.56", Decimal("4.56")],
+            ],
+            index=["A", "B"],
+            columns=["X", "Y", "Z"],
+        )
+        df.to_excel(tmp_excel)
+
+        with ExcelFile(tmp_excel) as reader:
+            result = pd.read_excel(reader, index_col=0, dtype=object)
+
+        expected = DataFrame(
+            [
+                [1.23, "1.23", 1.23],
+                [4.56, "4.56", 4.56],
+            ],
+            index=["A", "B"],
+            columns=["X", "Y", "Z"],
+            dtype=object,
+        )
+        tm.assert_frame_equal(result, expected)
+
     def test_to_excel_output_encoding(self, tmp_excel):
         # Avoid mixed inferred_type.
         df = DataFrame(