The previous answer by Paul fails the test round_down(4.6, 0.2) == 4.6.
This answer has two types of solutions, inexact and exact. They pass all previous tests and more, also with negative numbers. Each approach provides solutions for round_nearest, round_down, and round_up.
As a disclaimer, these solutions require a lot more testing. Where math.isclose is used, its default tolerances apply.
Can you find a failing example?
To devise additional exact solutions, consider this reference.
Using round (inexact)
import math
def round_nearest(num: float, to: float) -> float:
    return round(num / to) * to  # Credited to Paul H.
def round_down(num: float, to: float) -> float:
    nearest = round_nearest(num, to)
    if math.isclose(num, nearest): return num
    return nearest if nearest < num else nearest - to
def round_up(num: float, to: float) -> float:
    nearest = round_nearest(num, to)
    if math.isclose(num, nearest): return num
    return nearest if nearest > num else nearest + to
# Tests:
rn, rd, ru = round_nearest, round_down, round_up
> rn(1.27, 0.05), rn(1.29, 0.05), rn(1.30, 0.05), rn(1.39, 0.05)
(1.25, 1.3, 1.3, 1.4000000000000001)
> rn(-1.27, 0.05), rn(-1.29, 0.05), rn(-1.30, 0.05), rn(-1.39, 0.05)
(-1.25, -1.3, -1.3, -1.4000000000000001)
> rn(4.4, 0.2), rn(4.5, 0.2), rn(4.6, 0.2)
(4.4, 4.4, 4.6000000000000005)
> rn(-4.4, 0.2), rn(-4.5, 0.2), rn(-4.6, 0.2)
(-4.4, -4.4, -4.6000000000000005)
> rn(82, 4.3)
81.7
> rd(1.27, 0.05), rd(1.29, 0.05), rd(1.30, 0.05)
(1.25, 1.25, 1.3)
> rd(-1.27, 0.05), rd(-1.29, 0.05), rd(-1.30, 0.05)
(-1.3, -1.3, -1.3)
> rd(4.4, 0.2), rd(4.5, 0.2), rd(4.6, 0.2)
(4.4, 4.4, 4.6)
> rd(-4.4, 0.2), rd(-4.5, 0.2), rd(-4.6, 0.2)
(-4.4, -4.6000000000000005, -4.6)
> ru(1.27, 0.05), ru(1.29, 0.05), ru(1.30, 0.05)
(1.3, 1.3, 1.3)
> ru(-1.27, 0.05), ru(-1.29, 0.05), ru(-1.30, 0.05)
(-1.25, -1.25, -1.3)
> ru(4.4, 0.2), ru(4.5, 0.2), ru(4.6, 0.2)
(4.4, 4.6000000000000005, 4.6)
> ru(-4.4, 0.2), ru(-4.5, 0.2), ru(-4.6, 0.2)
(-4.4, -4.4, -4.6)
import math
def round_down(num: float, to: float) -> float:
    if num < 0: return -round_up(-num, to)
    mod = math.fmod(num, to)
    return num if math.isclose(mod, to) else num - mod
def round_up(num: float, to: float) -> float:
    if num < 0: return -round_down(-num, to)
    down = round_down(num, to)
    return num if num == down else down + to
def round_nearest(num: float, to: float) -> float:
    down, up = round_down(num, to), round_up(num, to)
    return down if ((num - down) < (up - num)) else up
# Tests:
rd, ru, rn = round_down, round_up, round_nearest
> rd(1.27, 0.05), rd(1.29, 0.05), rd(1.30, 0.05)
(1.25, 1.25, 1.3)
> rd(-1.27, 0.05), rd(-1.29, 0.05), rd(-1.30, 0.05)
(-1.3, -1.3, -1.3)
> rd(4.4, 0.2), rd(4.5, 0.2), rd(4.6, 0.2)
(4.4, 4.4, 4.6)
> rd(-4.4, 0.2), rd(-4.5, 0.2), rd(-4.6, 0.2)
(-4.4, -4.6000000000000005, -4.6)
> ru(1.27, 0.05), ru(1.29, 0.05), ru(1.30, 0.05)
(1.3, 1.3, 1.3)
> ru(-1.27, 0.05), ru(-1.29, 0.05), ru(-1.30, 0.05)
(-1.25, -1.25, -1.3)
> ru(4.4, 0.2), ru(4.5, 0.2), ru(4.6, 0.2)
(4.4, 4.6000000000000005, 4.6)
> ru(-4.4, 0.2), ru(-4.5, 0.2), ru(-4.6, 0.2)
(-4.4, -4.4, -4.6)
> rn(1.27, 0.05), rn(1.29, 0.05), rn(1.30, 0.05), rn(1.39, 0.05)
(1.25, 1.3, 1.3, 1.4000000000000001)
> rn(-1.27, 0.05), rn(-1.29, 0.05), rn(-1.30, 0.05), rn(-1.39, 0.05)
(-1.25, -1.3, -1.3, -1.4000000000000001)
> rn(4.4, 0.2), rn(4.5, 0.2), rn(4.6, 0.2)
(4.4, 4.4, 4.6)
> rn(-4.4, 0.2), rn(-4.5, 0.2), rn(-4.6, 0.2)
(-4.4, -4.4, -4.6)
> rn(82, 4.3)
81.7
This section implements only round_nearest. For round_down and round_up, use the same exact logic as in the "Using round" section.
def round_nearest(num: float, to: float) -> float:
    return num - math.remainder(num, to)
# Tests:
rn = round_nearest
> rn(1.27, 0.05), rn(1.29, 0.05), rn(1.30, 0.05), rn(1.39, 0.05)
(1.25, 1.3, 1.3, 1.4000000000000001)
> rn(-1.27, 0.05), rn(-1.29, 0.05), rn(-1.30, 0.05), rn(-1.39, 0.05)
(-1.25, -1.3, -1.3, -1.4000000000000001)
> rn(4.4, 0.2), rn(4.5, 0.2), rn(4.6, 0.2)
(4.4, 4.4, 4.6000000000000005)
> rn(-4.4, 0.2), rn(-4.5, 0.2), rn(-4.6, 0.2)
(-4.4, -4.4, -4.6000000000000005)
> rn(82, 4.3)
81.7
Note that this is an inefficient solution because it uses str.
from decimal import Decimal
import math
def round_nearest(num: float, to: float) -> float:
    num, to = Decimal(str(num)), Decimal(str(to))
    return float(round(num / to) * to)
def round_down(num: float, to: float) -> float:
    num, to = Decimal(str(num)), Decimal(str(to))
    return float(math.floor(num / to) * to)
def round_up(num: float, to: float) -> float:
    num, to = Decimal(str(num)), Decimal(str(to))
    return float(math.ceil(num / to) * to)
# Tests:
rn, rd, ru = round_nearest, round_down, round_up
> rn(1.27, 0.05), rn(1.29, 0.05), rn(1.30, 0.05), rn(1.39, 0.05)
(1.25, 1.3, 1.3, 1.4)
> rn(-1.27, 0.05), rn(-1.29, 0.05), rn(-1.30, 0.05), rn(-1.39, 0.05)
(-1.25, -1.3, -1.3, -1.4)
> rn(4.4, 0.2), rn(4.5, 0.2), rn(4.6, 0.2)
(4.4, 4.4, 4.6)
> rn(-4.4, 0.2), rn(-4.5, 0.2), rn(-4.6, 0.2)
(-4.4, -4.4, -4.6)
> rn(82, 4.3)
81.7
> rd(1.27, 0.05), rd(1.29, 0.05), rd(1.30, 0.05)
(1.25, 1.25, 1.3)
> rd(-1.27, 0.05), rd(-1.29, 0.05), rd(-1.30, 0.05)
(-1.3, -1.3, -1.3)
> rd(4.4, 0.2), rd(4.5, 0.2), rd(4.6, 0.2)
(4.4, 4.4, 4.6)
> rd(-4.4, 0.2), rd(-4.5, 0.2), rd(-4.6, 0.2)
(-4.4, -4.6, -4.6)
> ru(1.27, 0.05), ru(1.29, 0.05), ru(1.30, 0.05)
(1.3, 1.3, 1.3)
> ru(-1.27, 0.05), ru(-1.29, 0.05), ru(-1.30, 0.05)
(-1.25, -1.25, -1.3)
> ru(4.4, 0.2), ru(4.5, 0.2), ru(4.6, 0.2)
(4.4, 4.6, 4.6)
> ru(-4.4, 0.2), ru(-4.5, 0.2), ru(-4.6, 0.2)
(-4.4, -4.4, -4.6)
Note that this is an inefficient solution because it uses str. Its test results are identical to those in the "Using decimal.Decimal" section. In my benchmarks, approaches using Fraction were much slower than those using Decimal.
from fractions import Fraction
import math
def round_nearest(num: float, to: float) -> float:
    num, to = Fraction(str(num)), Fraction(str(to))
    return float(round(num / to) * to)
def round_down(num: float, to: float) -> float:
    num, to = Fraction(str(num)), Fraction(str(to))
    return float(math.floor(num / to) * to)
def round_up(num: float, to: float) -> float:
    num, to = Fraction(str(num)), Fraction(str(to))
    return float(math.ceil(num / to) * to)