I have recently created the Money
class with a helper class Currency
. What do you think about the following code? I created unit tests for my class using very nice pytest
library.
Is it a good idea to put code tests in the if __name__ == '__main__'
or should I always use dedicated libraries like unittest
and pytest
?
Is it okay to make self
attribute assignment depend on the another attribute assignment, for example let's suppose we have an assignment self.amount = default_value
depending on whether self.precision
is defined first, where self.amount
is property setter that uses self.precision
?
How can I simplify repeating decorators code like I have in the test_money.py
?
If I am going to gather exchange rate info from the Internet, should I still use namedtuple
or use dataclasses
?
Thanks
money.py
:
import functools
from decimal import Decimal
from dataclasses import dataclass
@dataclass
class Currency:
name: str
symbol: str
exchange_rate: Decimal
def __str__(self) -> str:
return self.name
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.name!r}, {self.symbol!r}, {self.exchange_rate!r})"
@functools.total_ordering
class Money:
def __init__(self, amount: Decimal, currency: Currency, precision: int = 2):
self._amount = amount
self.precision = precision
self.currency = currency
@property
def amount(self) -> Decimal:
return self._amount.quantize(Decimal("10") ** -self.precision)
@amount.setter
def amount(self, value: Decimal) -> None:
self._amount = value
def __str__(self) -> str:
return f"{self.amount} {self.currency.symbol}"
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.amount!r}, {self.currency!r})"
def __add__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot add two {self.__class__.__name__} instances: "
"different currency property")
return Money(self.amount + other.amount, self.currency)
def __sub__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot subtract two {self.__class__.__name__} instances: "
"different currency property")
return Money(self.amount - other.amount, self.currency)
def __mul__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot multiply two {self.__class__.__name__} instances: "
"different currency property")
return Money(self.amount * other.amount, self.currency, self.precision)
def __truediv__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot divide two {self.__class__.__name__} instances: "
"different currency property")
return Money(self.amount / other.amount, self.currency, self.precision)
def __iadd__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot add two {self.__class__.__name__} instances: "
"different currency property")
self.amount += other.amount
return self
def __isub__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot subtract two {self.__class__.__name__} instances: "
"different currency property")
self.amount -= other.amount
return self
def __imul__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot multiply two {self.__class__.__name__} instances: "
"different currency property")
self.amount *= other.amount
return self
def __idiv__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot divide two {self.__class__.__name__} instances: "
"different currency property")
self.amount /= other.amount
return self
def __lt__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot compare two {self.__class__.__name__} instances: "
"different currency property")
return self.amount < other.amount
def __eq__(self, other):
if self.currency != other.currency:
raise ValueError(f"cannot compare two {self.__class__.__name__} instances: "
"different currency property")
return self.amount == other.amount
test_money.py
:
import pytest
from example_shop.shop.money import Currency, Money
from decimal import Decimal
from typing import Optional
def money_euro(amount: str, precision: Optional[int] = None) -> Money:
if precision is None:
return Money(Decimal(amount), Currency("Euro", "EUR", Decimal("4.52")))
return Money(Decimal(amount), Currency("Euro", "EUR", Decimal("4.52")), precision)
def money_usd(amount: str, precision: Optional[int] = None) -> Money:
if precision is None:
return Money(Decimal(amount), Currency("American dollar", "USD", Decimal("4.17")))
return Money(Decimal(amount), Currency("American dollar", "USD", Decimal("4.17")), precision)
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("2.7")),
(money_usd("2.7"), money_usd("1.3"), money_usd("4.0")),
(money_usd("2.700"), money_usd("1.3"), money_usd("4.00000")),
(money_usd("1.5", 4), money_usd("1.5", 3), money_usd("3", 4)),
(money_usd("-1.5", 4), money_usd("3", 5), money_usd("1.5", 5))])
def test_money_add_the_same_currency(price1, price2, expected):
assert price1 + price2 == expected
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("0.3")),
(money_usd("2.7"), money_usd("1.3"), money_usd("1.4")),
(money_usd("2.700"), money_usd("1.3"), money_usd("1.40000")),
(money_usd("1.5", 4), money_usd("1.5", 3), money_usd("0", 4)),
(money_usd("1.5", 4), money_usd("3", 5), money_usd("-1.5", 5))])
def test_money_subtract_the_same_currency(price1, price2, expected):
assert price1 - price2 == expected
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("1.8")),
(money_usd("2.7"), money_usd("1.3"), money_usd("3.51")),
(money_usd("2.700"), money_usd("1.3"), money_usd("3.51000")),
(money_usd("0", 4), money_usd("1.5", 3), money_usd("0", 4)),
(money_usd("1.5", 4), money_usd("-3", 5), money_usd("-4.5", 5))])
def test_money_multiply_the_same_currency(price1, price2, expected):
assert price1 * price2 == expected
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("1.25")),
(money_usd("2.7"), money_usd("1.3"), money_usd("2.08")),
(money_usd("2.700"), money_usd("1.3"), money_usd("2.08000")),
(money_usd("0", 4), money_usd("1.5", 3), money_usd("0", 4)),
(money_usd("1.5", 4), money_usd("-3", 5), money_usd("-0.5", 5))])
def test_money_divide_the_same_currency(price1, price2, expected):
assert price1 / price2 == expected
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_add_the_different_currency(price1, price2):
with pytest.raises(ValueError):
assert price1 + price2
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_subtract_different_currency(price1, price2):
with pytest.raises(ValueError):
assert price1 - price2
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_multiply_different_currency(price1, price2):
with pytest.raises(ValueError):
assert price1 * price2
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_divide_different_currency(price1, price2):
with pytest.raises(ValueError):
assert price1 / price2
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("2.7")),
(money_usd("2.7"), money_usd("1.3"), money_usd("4.0")),
(money_usd("2.700"), money_usd("1.3"), money_usd("4.00000")),
(money_usd("1.5", 4), money_usd("1.5", 3), money_usd("3", 4)),
(money_usd("-1.5", 4), money_usd("3", 5), money_usd("1.5", 5))])
def test_money_add_in_place_the_same_currency(price1, price2, expected):
result = price1
result += price2
assert result == expected
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("0.3")),
(money_usd("2.7"), money_usd("1.3"), money_usd("1.4")),
(money_usd("2.700"), money_usd("1.3"), money_usd("1.40000")),
(money_usd("1.5", 4), money_usd("1.5", 3), money_usd("0", 4)),
(money_usd("1.5", 4), money_usd("3", 5), money_usd("-1.5", 5))])
def test_money_subtract_in_place_the_same_currency(price1, price2, expected):
result = price1
result -= price2
assert result == expected
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("1.8")),
(money_usd("2.7"), money_usd("1.3"), money_usd("3.51")),
(money_usd("2.700"), money_usd("1.3"), money_usd("3.51000")),
(money_usd("0", 4), money_usd("1.5", 3), money_usd("0", 4)),
(money_usd("1.5", 4), money_usd("-3", 5), money_usd("-4.5", 5))])
def test_money_multiply_in_place_the_same_currency(price1, price2, expected):
result = price1
result *= price2
assert result == expected
@pytest.mark.parametrize("price1,price2,expected",
[(money_usd("1.5"), money_usd("1.2"), money_usd("1.25")),
(money_usd("2.7"), money_usd("1.3"), money_usd("2.08")),
(money_usd("2.700"), money_usd("1.3"), money_usd("2.08000")),
(money_usd("0", 4), money_usd("1.5", 3), money_usd("0", 4)),
(money_usd("1.5", 4), money_usd("-3", 5), money_usd("-0.5", 5))])
def test_money_divide_in_place_the_same_currency(price1, price2, expected):
result = price1
result /= price2
assert result == expected
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_add_in_place_the_different_currency(price1, price2):
with pytest.raises(ValueError):
result = price1
result += price2
assert result
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_subtract_in_place_different_currency(price1, price2):
with pytest.raises(ValueError):
result = price1
result -= price2
assert result
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_multiply_in_place_different_currency(price1, price2):
with pytest.raises(ValueError):
result = price1
result *= price2
assert result
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.5"), money_euro("1.2")),
(money_euro("1.2"), money_usd("1.5")),
(money_usd("1.5", 4), money_euro("1.2", 5)),
(money_euro("1.2", 4), money_usd("1.5", 5))])
def test_money_divide_in_place_different_currency(price1, price2):
with pytest.raises(ValueError):
result = price1
result /= price2
assert result
@pytest.mark.parametrize("price1,price2,expected",
[(money_euro("1.23"), money_euro("4.56"), True),
(money_euro("1.5"), money_euro("1"), False),
(money_usd("-2"), money_usd("0"), True),
(money_euro("0"), money_euro("0"), False)])
def test_less_than_the_same_currency(price1, price2, expected):
assert (price1 < price2) == expected
@pytest.mark.parametrize("price1,price2,expected",
[(money_euro("1.23"), money_euro("4.56"), False),
(money_euro("1.5"), money_euro("1"), False),
(money_usd("-2"), money_usd("0"), False),
(money_euro("0"), money_euro("0"), True)])
def test_equal_the_same_currency(price1, price2, expected):
assert (price1 == price2) == expected
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.23"), money_euro("4.56")),
(money_euro("1.5"), money_usd("1"))])
def test_less_than_different_currency(price1, price2):
with pytest.raises(ValueError):
assert price1 < price2
@pytest.mark.parametrize("price1,price2",
[(money_usd("1.23"), money_euro("4.56")),
(money_euro("1.5"), money_usd("1"))])
def test_equal_different_currency(price1, price2):
with pytest.raises(ValueError):
assert price1 == price2
3 Answers 3
The Currency
class is simple and nice.
Thank you for the unit tests! They are helpful.
exchange_rate: Decimal
I have no idea what that means, and the comments, docstrings, narrative do not enlighten me.
Is that a rate to convert the given currency to Yen, to Dinar, to something else? That is, are we measuring a US Dollar -> Yen rate? A Euro -> Yen rate?
Are you sure this attribute is really needed here? Consider removing it. Currency conversion can be a complex topic, for example the time and place of conversion make a difference. It probably belongs in a separate module.
I am uncomfortable with rounding in the Money
class.
Also, I wouldn't mind seeing both
Money(Decimal(1/8), usd)
andMoney('0.125', usd)
behave identically, for the sake of the caller's convenience.
That is, I think of a "money" as a "decimal" quantity,
so asking me to use it in a different way doesn't quite feel right.
Perhaps Currency
should always offer a helper
that constructs a new Money
?
I appreciate that we consistently round to precision
on the way out,
e.g. with expressions like invoice.amount
or str(invoice)
.
But I'm not sure how an app developer is supposed to reason
about a quantity like this:
invoice = Money(Decimal('.12'), usd)
invoice += Money(Decimal('.135'), usd)
At this point str(invoice)
will reveal pennies,
despite having precision to mils on the inside.
(BTW, fun fact, '.125' --> .12 while '.135' --> .14, which is correct round-to-even behavior.)
I think my objection boils down to discomfort with tracking two distinct quanitities:
- public
invoice.precision == -2
above, yet invoice._amount.as_tuple().exponent == -3
I don't see a motivating use case for this in the
narrative nor in the test suite.
I think I recommend abandoning the redundant invoice.precision
attribute entirely.
I'm not happy that an app developer can silently misuse
the ctor and misuse the setter.
There's no checking, no duck typed operation,
which immediately blows up when e.g. a str
is passed in rather than a Decimal
.
Simplest way to fix? I recommend that you
assign Decimal(amount)
and assign Decimal(value)
,
silently "fixing" it on the way in.
Consider raising an error if someone foolishly
passes in a float, e.g. Money(1/8, usd)
or
Money(0.10, usd)
probably won't do what developer intended,
and he will thank you for immediately pointing that out.
I like the "currencies must match!" rule for addition. Let the app developer do an approximate conversion rate calculation if he really wants to add one dollar to one euro.
It's odd that __sub__
doesn't just __add__
with
flipped sign. That would let you code the "must match!"
test just once.
What's going on with mul & div ?!?
That's just crazy.
Unconditionally raise
an error and be done with it.
You export .amount
and that suffices.
Nice use of total_ordering
!
In the money_euro
test helper,
special treatment for precision
of None
is a bit odd.
Consider defining a kw
dict which is
either empty or contains a numeric value,
and finish the Money
call with **kw)
Consider defining eur
and usd
in the test suite or perhaps in the money module.
Tests are the one place where "copy-n-paste is OK". But I found that the repeated pasting of four money pairs distracted me from easily reading what mattered, which is the differences between the various tests. Assign the list to a variable so you can concisely refer to it.
Consider writing a hypothesis test for this module.
-
\$\begingroup\$ Should I compare in
__eq__
and__lt__
only ifself.amount
is equal/less thanother.amount
and raise an error if objects have a different currencies or do conversions to common currency, for instance USD and compare accordingly objects? \$\endgroup\$whiteman808– whiteman8082023年06月03日 18:28:04 +00:00Commented Jun 3, 2023 at 18:28 -
\$\begingroup\$ Tell the app developer that comparing apple to orange, or dollar to euro, is
raise TypeError("not supported")
. Otherwise we need to worry about buy-side vs sell-side conversion rate, documentation, and lots of fractional digits. There's no motivating use case, so just bail, don't even go there. The app developer can accessinvoice.amount
to convert currencies to his heart's content. \$\endgroup\$J_H– J_H2023年06月03日 18:32:40 +00:00Commented Jun 3, 2023 at 18:32 -
\$\begingroup\$ Do you think that I should in the constructor assign to
self.amount = Decimal(amount)
? I did a comparisonif not isinstance(amount, str): raise TypeError(...)
, and made type annotationamount: str
. Is it a good way? \$\endgroup\$whiteman808– whiteman8082023年06月03日 18:36:12 +00:00Commented Jun 3, 2023 at 18:36 -
\$\begingroup\$ The concern I gave voice to was that ctor and setter might assign
self._amount = ".125"
if an app developer mistakenly passed that in. You document that caller must supplyamount: Decimal
, and I recommend you enforce thatself._amount
shall always be a Decimal. We call this a Class Invariant. Documenting something more permissive, likeamount: str | Decimal
, would be fine. I also mentioned that we should probablyraise
if app developer mistakenly passes in a float. I'm just trying to anticipate how folks would misuse the module, and how hard the bugs are to find. \$\endgroup\$J_H– J_H2023年06月03日 18:41:39 +00:00Commented Jun 3, 2023 at 18:41 -
\$\begingroup\$ I have answered my own question. Is the mine proposed code better? \$\endgroup\$whiteman808– whiteman8082023年06月03日 18:47:21 +00:00Commented Jun 3, 2023 at 18:47
Other answers (and comments that should be answers) have covered most of my concerns.
This does not follow how most locales would prefer to format their currency:
f"{self.amount} {self.currency.symbol}"
Instead use locale
support, which (among other things) will give you your international currency name, proper formatting, grouping etc. Also, you should either freeze your currency dataclass or use NamedTuple
instead. As a demonstration,
import locale
from contextlib import contextmanager
from decimal import Decimal
from functools import lru_cache
from numbers import Number
from pprint import pprint
from typing import NamedTuple, Iterator, Any
class Currency(NamedTuple):
value_eur: Decimal
display_locale: str
@contextmanager
def use_locale(self) -> Iterator:
"""This is not thread-safe"""
old_locale = locale.getlocale(category=locale.LC_MONETARY)
try:
locale.setlocale(category=locale.LC_MONETARY, locale=self.display_locale)
yield
finally:
locale.setlocale(category=locale.LC_MONETARY, locale=old_locale)
@property
@lru_cache(maxsize=1)
def localeconv(self) -> dict[str, Any]:
"""A dictionary of int_curr_symbol, currency_symbol, etc. For details see
https://docs.python.org/3/library/locale.html#locale.localeconv
"""
with self.use_locale():
return locale.localeconv()
def __str__(self) -> str:
return self.localeconv['int_curr_symbol']
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.display_locale!r}, {self.value_eur!r})"
def format(
self, val: Number,
symbol: bool = True, grouping: bool = True, international: bool = False,
) -> str:
with self.use_locale():
return locale.currency(val=val, symbol=symbol, grouping=grouping,
international=international)
def __truediv__(self, other: 'Currency') -> Decimal:
return self.value_eur / other.value_eur
cad = Currency(display_locale='en_CA', value_eur=Decimal(0.69680235))
inr = Currency(display_locale='ta_IN', value_eur=Decimal(0.011331307))
pprint(inr.localeconv)
print(cad.format(23_400_000))
print(inr.format(23_400_000))
print(
cad.format(1, international=True), 'is worth',
inr.format(cad/inr, international=True),
)
{'currency_symbol': '₹',
'decimal_point': '.',
'frac_digits': 2,
'grouping': [],
'int_curr_symbol': 'INR',
'int_frac_digits': 2,
'mon_decimal_point': '.',
'mon_grouping': [3, 2, 0],
'mon_thousands_sep': ',',
'n_cs_precedes': 1,
'n_sep_by_space': 1,
'n_sign_posn': 4,
'negative_sign': '-',
'p_cs_precedes': 1,
'p_sep_by_space': 1,
'p_sign_posn': 4,
'positive_sign': '',
'thousands_sep': ''}
23,400,000ドル.00
₹ 2,34,00,000.00
CAD1.00 is worth INR61.49
Edit made to code:
money.py
:
import functools
from decimal import Decimal
from typing import NamedTuple
class Currency(NamedTuple):
name: str
symbol: str
def __str__(self) -> str:
return self.name
def __repr__(self) -> str:
return f"{self.__class__.__name__}(name={self.name!r}, symbol={self.symbol!r})"
@functools.total_ordering
class Money:
def __init__(self, amount: Decimal, currency: Currency):
if isinstance(amount, float):
raise TypeError("amount should not be float")
self.amount = Decimal(amount)
self.currency = currency
def __str__(self) -> str:
return f"{self.amount} {self.currency.symbol}"
def __repr__(self) -> str:
return f"{self.__class__.__name__}(amount={self.amount!r}, currency={self.currency!r})"
def __lt__(self, other):
if self.currency != other.currency:
raise ValueError("comparison between different currencies is not supported")
return self.amount < other.amount
def __eq__(self, other):
if self.currency != other.currency:
raise ValueError("comparison between different currencies is not supported")
return self.amount == other.amount
EUR = Currency("Euro", "EUR")
USD = Currency("American dollar", "USD")
test_money.py
:
import pytest
from example_shop.shop.money import Money, EUR, USD
from decimal import Decimal
@pytest.mark.parametrize("price1,price2,expected",
[(Money(Decimal("1.23"), EUR), Money(Decimal("4.56"), EUR), True),
(Money(Decimal("1.5"), EUR), Money(Decimal("1"), EUR), False),
(Money(Decimal("-2"), USD), Money(Decimal("0"), USD), True),
(Money(Decimal("0"), EUR), Money(Decimal("0"), EUR), False)])
def test_less_than_the_same_currency(price1, price2, expected):
assert (price1 < price2) == expected
@pytest.mark.parametrize("price1,price2,expected",
[(Money(Decimal("1.23"), USD), Money(Decimal("4.56"), USD), False),
(Money(Decimal("1.5"), EUR), Money(Decimal("1"), EUR), False),
(Money(Decimal("-2"), EUR), Money(Decimal("0"), EUR), False),
(Money(Decimal("0"), USD), Money(Decimal("0"), USD), True)])
def test_equal_the_same_currency(price1, price2, expected):
assert (price1 == price2) == expected
@pytest.mark.parametrize("price1,price2",
[(Money(Decimal("1.23"), EUR), Money(Decimal("4.56"), USD)),
(Money(Decimal("1.5"), USD), Money(Decimal("1"), EUR))])
def test_less_than_different_currency(price1, price2):
with pytest.raises(ValueError):
assert price1 < price2
@pytest.mark.parametrize("price1,price2",
[(Money(Decimal("1.23"), EUR), Money(Decimal("4.56"), USD)),
(Money(Decimal("1.5"), USD), Money(Decimal("1"), EUR))])
def test_equal_different_currency(price1, price2):
with pytest.raises(ValueError):
assert price1 == price2
-
\$\begingroup\$ Better you edit your question, instead of posting this as answer :) \$\endgroup\$Billal BEGUERADJ– Billal BEGUERADJ2023年06月03日 19:01:08 +00:00Commented Jun 3, 2023 at 19:01
-
\$\begingroup\$ I'll remember next time \$\endgroup\$whiteman808– whiteman8082023年06月03日 19:12:50 +00:00Commented Jun 3, 2023 at 19:12
-
4\$\begingroup\$ @BillalBegueradj, I know you're trying to be helpful, and I thank you for that. On SO sometimes appending "I found it!" to a question is most helpful, and sometimes a self-answer works best. But CodeReview is slightly different. I direct your attention to item 6: "After an answer is posted, you must not edit your question to invalidate any advice." If OP revises code in the question, such edits will soon be reverted. The idea is so folks reading a year later will see the original text + suggestions. \$\endgroup\$J_H– J_H2023年06月03日 19:50:39 +00:00Commented Jun 3, 2023 at 19:50
-
\$\begingroup\$ Thanks for posting this code. It's a good idea to summarise which changes you made, and why - a self-answer ought to review the code, just like any other answer. \$\endgroup\$Toby Speight– Toby Speight2023年06月04日 18:18:09 +00:00Commented Jun 4, 2023 at 18:18
-
\$\begingroup\$ @TobySpeight this is not an answer (?) \$\endgroup\$Billal BEGUERADJ– Billal BEGUERADJ2023年06月04日 19:39:07 +00:00Commented Jun 4, 2023 at 19:39
Explore related questions
See similar questions with these tags.
Money
andCurrency
a named tuples and define functionconvert_currency
? \$\endgroup\$