Python knows int. Python knows float. It doesn't know frac. You'll teach it!
How? Create a frac class of course. (That's short for "fraction"; and yes, I want you not to capitalize.)
What should that class contain? The Tests below are your best guide. Study them closely. But do let me give you a bit of direction. (I assume you've done Chapter 10. If not, go do it!)
You will of course write a constructor. This will be the method named __init__. As we learned, this is method that will be called when we call the class; that is, a line that contains frac(n, d) (where n and d represent numerator and denominator) will result in a call to __init__, and n and d will become the values of its parameters.
What will the constructor do? Well, it will set values for the fraction's numerator and denominator. To do this, I suggest that you create attributes named numer and denom. Thus somewhere in your __init__ method, you will have lines like this:
self.numer = ...
self.denom = ...
I do have a number of suggestions about how you set the values of the attributes numer and denom. I almost feel as I need to apologize for the complexity here, but honestly fractions are a bit complex! (When you do the Complex Numbers project, you'll find that complex numbers are, contrary to their name, considerably less complex.)
(added 4.28.2025) Fractions can be created from either a pair of ints, a pair of fractions or an integer and a fraction (in some order).
If the user tries to create a fraction with a numer and denom that aren't ints or fractions, throw an error. Here's a cool line of code that will do that: assert isinstance(numer, int) and assert isinstance(denom, int). In the assert statement, the assert must be followed by a Boolean expression. If that Boolean is True, the line does nothing and execution skips to the next line. But if the Boolean is False, the code ceases execution and exits with an error message. Alternatively, you could raise a ValueError if you like.
You should automatically reduce your fractions when they're created. That means you'll need a helper function to reduce. That's Greatest Common Factor (which you wrote in a previous chapter); to reduce a fraction means to divide (// division of course) top and bottom by their GCF.
If the second argument sent to the constructor is an int, assume that it's the numerator and that the denominator is 1. How will you do this? Set a default value for the denominator parameter. (We've seen this before. If you've forgotten how, a quick Google search will remind you.)
I suggest you store a negative fraction as a negative numerator over a positive denominator. I did.
If __init__ is sent two negative values, I think it sensible to make both numerator and denominator positive.
Of course the denominator can't be 0. The attempt to make it 0 should result in an error.
You'll overload various built-in operators - + for addition, - for subtraction, * for multiplication, / for division, // for floor division (which always yields an integer, % for modulus (i.e., remainder), ** for exponentiation, == for equality comparison, != for inequality, and > for greater than, < for less than, >= for greater than or equal to and <= for less than or equal to. Remember that to do that you'll need the double-underscores functions.
Here's the complete list of the dunder methods you'll write:
__init__, __repr__, __lt__, __le__, __eq__, __ne__, __gt__, __ge__, __add__, __radd__, __sub__, __rsub__, __mul__, __rmul__, __truediv__, __rtruediv__, __pow__, __mod__, __rmod__, __floordiv__, __rfloordiv__, __neg__, __abs__, __int__, __float__.
Let me make a few observations/suggestions here.
Implemented by __add__, __radd__, __sub__ and __rsub__.
I'll comment on addition only. The same comments could also be made about subtraction.
To add, you'll need to find a common denominator. You can use Least Common Multiple for this. (You can but needn't. All you really need is a common multiple; and one sure way to get that is to multiply denominators. But that's not elegant! It is however often faster.) Your wrote LCM in a previous chapter.
You should be able to add a frac to a non-frac number (by which I mean an int or a float) where the frac occurs on either the left or right. If it occurs on the left, your __add__ method will handle the addition; if it occurs on the right, it will be __radd__. (Recall that when we end up in a dunder-r method like __radd__, the arguments come into the function reversed.)
If an int is added to a frac, the int should be converted to a frac and then the two fracs should be added. If a float is added to a frac, the frac should be converted to an float and then the floats added. (In general, if we ever combine a frac and a float by means of some operation, the result should be a float.)
The comments above about addition should answer most questions about multiplication and division. But I should make two additional points:
__truediv__ and __rtruediv__ are for single-slash division, i.e. m / n.
m // n, i.e. floor division, can be defined as the greatest integer that is less than or equal to m / n.
m % n, i.e. remainder, can be defined as: m - (m // n) * n.
A fraction raised to a integer power should return a fraction. A fraction raised to a non-integer power should raise an exception. Note too that, in general, a**n equal 1 / a**abs(n) when n is negative.
Certain of the dunder methods will returns Booleans, not numbers. These are the comparison operators - ==, !=, <, >, <= and >=. Note:
Two fractions are equal if their cross-products are equal.
To determine whether one fraction is less than or greater than another, we can compare cross-products. I'll let you figure out how precisely to do that.
You should implement both int and float type conversion; the dunder methods __int__ and __float__ should be used. Int type conversion should yield the largest integer less than the given frac, and float type conversion should simply divide numerator by denominator.
Of your functions, only one will return a string. That's __repr__ of course. The rest will return numbers. Indeed most will return fracs. (Yes, that means that in most of your functions, you'll build and then return fracs.) I suggest your repr return a string of the form "numer / denom"; that seems the obvious choice. Recall that Python will use your repr to get a string representation of a fraction when it is printed.
I've seen a few students in the past mutate a fraction in one of their dunder methods. That would mean to change the values of its numer and denom attributes. Don't do that! Of course you could. For example, when you add, you could change the numer and/or denom of one of the fracs. But don't! Instead create a new frac and return that. Here's how to make sure you don't mutate: never have a line of code that beings a_frac.numer = or a_frac.denom = outside the __init__ method.
Really, this seems the only sensical policy. It's nonsense to say that a fraction can become another fraction. 1/2 can't become 2/3!
If we take a fraction and divide its numerator by its denominator by means of Python's / operator, we get a float. Often that float representation, though sometimes inexact, is quite good enough. But what if we wanted a decimal representation of a float that was always exactly right? Could we do that? Well yes, yes we can.
I'll need to review a bit of the math. Sometimes when we divide a fraction's numerator by its denominator, we get a decimal representation that terminates. For instance, 1/2 = 0.5, 4/5 = 0.8 and 3/25 = 0.12. These are all perfectly precise.
Sometimes however, when we divide numerator by denominator, we don't get a decimal that terminates. For instance, 1/3 = 0.333... and 2/9 = 0.222... . The dots of course indicate that the expansion continues on in the same way. To infinity. Thus the decimal expansion of 1/3 consists of an infinite sequence of 3's and the decimal expansion of 2/9 consists of an infinite sequence of 2's.
In other cases, we have a larger block of digits that repeats. (In the above two examples, the block the repeats - first a 3 and then a 2 - is just one digit long.) For instance, 1/7 = 0.142857142857..., where the block that repeats is the 142857.
It's usual in math to place a bar over the block that repeats. But we can't do that in Python. So let's instead place an "r" immediately before the chunk that repeats. So: 1/3 = 0.r3, 2/9 = 0.r2 and 1/7 = 0.r142857.
Do note that we can have a sequence of digits in front of the decimal point, and (perhaps not as obviously) we can have a sequence after the decimal point before the block that repeats. For instance, 147.238r1463 is the decimal representation of a certain fraction. (What fraction? This one: 5353579/36360. Confirm at Wolfram Alpha if you like.)
Now, here's your task. Write a function named exactDec that takes a fraction and produces the exact decimal representation of that fraction in the form of a string. Place an "r" immediately before the chunk that repeats, as we did above. Do note that every fraction can be represented in this way; that is, the decimal representation of every ratio of integers either eventually terminates or reaches a block of digits that, from there on, simply repeats.
exactDec should be a function within your frac class; that is, it should be a frac method.
You'll find a number of test cases below.
Write a function that reverses the exactDec function described above. Call it decToFrac. It should take a string that gives the exact decimal representation of a fraction and returns the fraction that it equals. As above, an "r" should be interpreted to mean that the digits that follow are the chunk that repeats. For instance, if you give it "147.238r1463", it should return the fraction 5353579/36360.
(I found the discussion in "What is Mathematics?", by Courant and Robbins to be quite helpful. See the section titled "Rational Numbers and Periodic Decimals". The authors derive a formula. I used it.)
I suggest that you not place decToFrac inside the frac class, since it's not a method that acts on fractions. I suggest that instead you place it before the frac class with the other helper functions (which likely include lcm and gcf.)
Below are the tests. If your code can generate output identical to mine, you should be good! (modified 4.28.2025)
import frac
print('A Few Fractions')
f1 = frac.frac(12, 15) # Reduce to 4/5
f2 = frac.frac(27, 8) # Irreducible
f3 = frac.frac(5, -2) # Neg fraction is always neg numer over pos denom
f4 = frac.frac(-5, -2) # Represent as 5/2
f5 = frac.frac(42) # Represent as 42/1
f6 = frac.frac(0) # Represent as 0/1
f7 = frac.frac(f1, f2) # A fraction created from fractions
f8 = frac.frac(3, f1) # A fraction created from an int and a frac
f9 = frac.frac(f1, 3)
print(f1, f2, f3, f4, f5, f6, f7, f8, f9)
print(type(f1), type(f5)) # Should be type frac.
print()
print("Negation and Absolute Value")
print(-f1, --f1)
print(abs(f1), abs(f3))
print()
print('Two-Frac Operations')
f0 = frac.frac(1)
f1 = frac.frac(1, 2)
f2 = frac.frac(-2, 3)
print(f1, '+', f2, '=', f1 + f2)
print(f1, '-', f2, '=', f1 - f2)
print(f1, '*', f2, '=', f1 * f2)
print(f1, '/', f2, '=', f1 / f2)
print(1, '//', f1, '=', 1 // f1)
print(f2, '%', f1, '=', f2 % f1)
print(f1, '** 3 =', f1 ** 3)
print(f1, '** -3 = ', f1 ** -3)
f2 = -f2
print()
print('Chained Operations')
f3 = frac.frac(5, 4)
print(f1, '+', f2, '*', f3, '=', f1 + f2 * f3)
print(f3, '/', f1, '-', f3, '=', f3 / f1 - f3)
print()
print('Operations with Ints and Floats')
# handle a non-frac on the left and on the right
# if an operand is a float, the output is a float
print('3 + ', f2, '=', 3 + f2)
print('3.0 + ', f2, '=', 3.0 + f2)
print(f2, '+ 3 =', f2 + 3)
print(f2, '+ 3.0 =', f2 + 3.0)
print('3 - ', f2, '=', 3 - f2)
print('3.0 - ', f2, '=', 3.0 - f2)
print(f2, '- 3 =', f2 - 3)
print(f2, '- 3.0 =', f2 - 3.0)
print('3 *', f2, '=', 3 * f2)
print('3.0 *', f2, '=', 3.0 * f2)
print(f2, '* 3 =', f2 * 3)
print(f2, '* 3.0 =', f2 * 3.0)
print('3 /', f2, '=', 3 / f2)
print('3.0 /', f2, '=', 3.0 / f2)
print(f2, '/ 3 =', f2 / 3)
print(f2, '/ 3.0 =', f2 / 3.0)
print('3 //', f1, '=', 3 // f1)
print('3 %', f3, '=', 3 % f3)
print()
print('Comparison')
f1 = frac.frac(2, 3)
f2 = frac.frac(3, 4)
print(f1 == f1, f1 != f1, f1 != f2)
print(f1 == 1, 1 == f1, f1 != 1, 1 != f1, f2 == 0.75, 0.75 == f2, f2 != 0.75, 0.75 != f2)
print(f1 < f2, f2 < f1, f1 < 1, f1 < 1.0, f1 <= f1, f2 <= f1, f2 <= 0.75, f2 <= 0.7)
print(0.7 < f1, 0.7 <= f2, 0 < f1, 1 <= f2)
print()
print("Type Conversion")
f1 = frac.frac(2, 3)
f2 = frac.frac(15, 4)
print(float(f1), float(f2))
print(int(f1), int(f2))
print()
print("Nasty fracs")
f1 = frac.frac(127, 2985)
f2 = frac.frac(3981, 371)
print(f1 ** 2 + f2)
f3 = frac.frac(1, 2)
f4 = frac.frac(2, 3)
f5 = frac.frac(3, 4)
print((f3 ** 2 + f4 ** 3) / (f5 - 5))
f6 = frac.frac(510510, 44100)
f7 = frac.frac(6636630, 573302)
print(f6 == f7, f6 < f7, f6 > f7, f6 - f7, float(f6 - f7))
print()
print("Fraction to Decimal")
f1 = frac.frac(1, 2)
f2 = frac.frac(1, 4)
f3 = frac.frac(1, 3)
f4 = frac.frac(1, 7)
f5 = frac.frac(24667, 19980)
f6 = frac.frac(41150, 33333)
print(f1, "=", f1.exactDec())
print(f2, "=", f2.exactDec())
print(f3, "=", f3.exactDec())
print(f4, "=", f4.exactDec())
print(f5, "=", f5.exactDec())
print(f6, "=", f6.exactDec())
print()
print("Decimal to Fraction")
d1 = "0.5"
d2 = "1.23r458"
d3 = "1.2345r12345"
d4 = "12.r9"
d5 = "12.12r9"
print(d1, "=", frac.decToFrac(d1))
print(d2, "=", frac.decToFrac(d2))
print(d3, "=", frac.decToFrac(d3))
print(d4, "=", frac.decToFrac(d4))
print(d5, "=", frac.decToFrac(d5))
My output:
A Few Fractions
4/5 27/8 -5/2 5/2 42 0 32/135 15/4 4/15
<class 'frac.frac'> <class 'frac.frac'>
Negation and Absolute Value
-4/5 4/5
4/5 5/2
Two-Frac Operations
1/2 + -2/3 = -1/6
1/2 - -2/3 = 7/6
1/2 * -2/3 = -1/3
1/2 / -2/3 = -3/4
1 // 1/2 = 2
-2/3 % 1/2 = 1/3
1/2 ** 3 = 1/8
1/2 ** -3 = 8
Chained Operations
1/2 + 2/3 * 5/4 = 4/3
5/4 / 1/2 - 5/4 = 5/4
Operations with Ints and Floats
3 + 2/3 = 11/3
3.0 + 2/3 = 3.6666666666666665
2/3 + 3 = 11/3
2/3 + 3.0 = 3.6666666666666665
3 - 2/3 = 7/3
3.0 - 2/3 = 2.3333333333333335
2/3 - 3 = -7/3
2/3 - 3.0 = -2.3333333333333335
3 * 2/3 = 2
3.0 * 2/3 = 2.0
2/3 * 3 = 2
2/3 * 3.0 = 2.0
3 / 2/3 = 9/2
3.0 / 2/3 = 4.5
2/3 / 3 = 2/9
2/3 / 3.0 = 0.2222222222222222
3 // 1/2 = 6
3 % 5/4 = 1/2
Comparison
True False True
False False True True True True False False
True False True True True False True False
False True True False
Type Conversion
0.6666666666666666 3.75
0 3
Nasty fracs
35477589584/3305693475
-59/459
False False True 2431/60196710 4.038426684780613e-05
Fraction to Decimal
1/2 = 0.5
1/4 = 0.25
1/3 = 0.r3
1/7 = 0.r142857
24667/19980 = 1.23r458
41150/33333 = 1.r23451
Decimal to Fraction
0.5 = 1/2
1.23r458 = 24667/19980
1.2345r12345 = 41150/33333
12.r9 = 13
12.12r9 = 1213/100