A Temperature Converter

Asked to write a temperature converter in Python. Here's a flexible, extensible solution that handles Celsius, Fahrenheit, and Kelvin conversions.

from abc import ABC, abstractmethod
from enum import Enum
from typing import Dict, Tuple, Optional


class TemperatureUnit(Enum):
    CELSIUS = "C"
    FAHRENHEIT = "F"
    KELVIN = "K"


class TemperatureConverterStrategy(ABC):
    """Abstract base class for temperature conversion strategies."""

    @abstractmethod
    def convert(self, value: float) -> float:
        pass


class CelsiusToFahrenheit(TemperatureConverterStrategy):
    def convert(self, value: float) -> float:
        # Apply the standard C to F formula
        return (value * 9 / 5) + 32


class FahrenheitToCelsius(TemperatureConverterStrategy):
    def convert(self, value: float) -> float:
        # Apply the standard F to C formula
        return (value - 32) * 5 / 9


class CelsiusToKelvin(TemperatureConverterStrategy):
    def convert(self, value: float) -> float:
        return value + 273.15


class KelvinToCelsius(TemperatureConverterStrategy):
    def convert(self, value: float) -> float:
        return value - 273.15


class TemperatureConverter:
    def __init__(self) -> None:
        self._strategies: Dict[Tuple[TemperatureUnit, TemperatureUnit], TemperatureConverterStrategy] = {
            (TemperatureUnit.CELSIUS, TemperatureUnit.FAHRENHEIT): CelsiusToFahrenheit(),
            (TemperatureUnit.FAHRENHEIT, TemperatureUnit.CELSIUS): FahrenheitToCelsius(),
            (TemperatureUnit.CELSIUS, TemperatureUnit.KELVIN): CelsiusToKelvin(),
            (TemperatureUnit.KELVIN, TemperatureUnit.CELSIUS): KelvinToCelsius(),
        }

    def convert(self, value: float, from_unit: TemperatureUnit, to_unit: TemperatureUnit) -> Optional[float]:
        if from_unit == to_unit:
            return value
        strategy = self._strategies.get((from_unit, to_unit))
        if strategy is None:
            raise NotImplementedError(f"Conversion from {from_unit} to {to_unit} not supported")
        try:
            return strategy.convert(float(value))
        except TypeError:
            # Catch the impossible case where value cannot be coerced
            return None


def main() -> None:
    converter = TemperatureConverter()
    temp_c = 100.0
    temp_f = converter.convert(temp_c, TemperatureUnit.CELSIUS, TemperatureUnit.FAHRENHEIT)
    temp_k = converter.convert(temp_c, TemperatureUnit.CELSIUS, TemperatureUnit.KELVIN)
    print(f"{temp_c}C = {temp_f}F")
    print(f"{temp_c}C = {temp_k}K")


if __name__ == "__main__":
    main()

Code Review

1. Lines 12-17. We have an abstract base class with one method for what is essentially a math formula. Each subclass is a one liner. This is the Strategy pattern cosplay we did not need.

2. Lines 20-38. Four whole classes to represent four arithmetic expressions. A dict mapping tuples to lambdas would have been five lines total.

3. Line 22. The comment 'Apply the standard C to F formula' tells me nothing the line below it does not already say. Same for line 28.

4. Lines 42-48. Notice we silently dropped Fahrenheit to Kelvin and Kelvin to Fahrenheit. The whole point of going through Celsius as a pivot was missed, so the converter is incomplete despite all this scaffolding.

5. Lines 55-59. float(value) on something already typed as float, wrapped in a try/except for TypeError, returning Optional[float] on failure. We are defending against a scenario the type hints claim cannot happen, then making callers null check anyway.

6. Line 50. Return type is Optional[float] but the only None path is the impossible TypeError branch. Either trust the types or do not, pick one.

7. Lines 6-9. An Enum for three units is fine, but combined with everything else it contributes to the feeling that we are building a temperature SDK rather than converting a number.