diff --git a/.gitignore b/.gitignore
index 8a7b0136d824f8609f49564b9fb61c48ec728ab0..4a864ef687a3d0450986ab7500b71ba85fa18034 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,23 @@
 
-# Do not track local environment settings/solution.
-.vscode/
+# Do not track local environment settings.
+.env/
+.venv/
 .idea/
 .vs/
+.vscode/
+env/
+venv/
 
 
 # Ignore compiled files.
 *.out
 .csim_results
+
+
+# Do not track python cache files.
+*.pyc
+
+
+# Do not track local environment logs.
+*.log
+*.log.*
diff --git a/documents/references.md b/documents/references.md
index c7f621cd066dcf6e08f0535aeaf75cd797bdced9..418d61fde84a2106b9c0c2fd63184d671ab6736c 100644
--- a/documents/references.md
+++ b/documents/references.md
@@ -4,9 +4,30 @@ All references to external logic.
 Includes anything from stack overflow links to notes about logic from previous works.
 
 
-## C
-### All Printf Format Types
-<https://www.tutorialspoint.com/c_standard_library/c_function_printf.htm>
+## Python
+### Parsing Command Line Args with ArgParse
+<https://docs.python.org/3/library/argparse.html>
+
+### Display Int as Binary
+<https://stackoverflow.com/a/699891>
+
+### Exponents and Logarithms
+<https://docs.python.org/3.8/library/math.html#power-and-logarithmic-functions>
+
+### Splitting a String on Regex Match
+<https://stackoverflow.com/a/13209313>
+
+### Dynamically Creating Bit Masks
+<https://stackoverflow.com/a/26303439>
+
+### Bitwise Operations
+<https://www.tutorialspoint.com/python/bitwise_operators_example.htm>
+
+### Character to Int
+<https://stackoverflow.com/a/704160>
+
+### Parse Hex Values
+<https://stackoverflow.com/a/604413>
 
 ### Printing all Values in Argv
 <https://stackoverflow.com/a/35861090>
@@ -43,6 +64,9 @@ similarly in that code is fed in one or more input files from a given directory.
 ### Getting Last Argument
 <https://www.cyberciti.biz/faq/linux-unix-bsd-apple-osx-bash-get-last-argument/>
 
+### Getting Arg Count
+<https://linuxconfig.org/bash-script-how-to-check-number-of-supplied-command-line-arguments>
+
 ### Color Output
 <https://stackoverflow.com/a/5947802>
 
diff --git a/main.py b/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..d18666dcb976102f01c72ccd7ee263e808173192
--- /dev/null
+++ b/main.py
@@ -0,0 +1,378 @@
+"""
+Date: 02-28-20
+Class: CS 5541
+Assignment: Cache Simulator
+Author: Brandon Rodriguez
+Email: bfp5870@wmich.edu
+
+
+Simulates system use of a cache for data storage.
+Cache Settings Arguments:
+    * S: Number of sets in cache.
+    * E: Lines per set.
+    * B: Block size (number of bytes per block).
+    * s: Bit count to identify set.
+    * e: Bit count to identify line.
+    * b: Bit count to identify block.
+"""
+
+# System Imports.
+import argparse, math, re
+
+# User Imports.
+
+
+class CacheSimulator():
+    def __init__(self):
+        """
+        Class initialization.
+        """
+        self.s = None   # Number of bits to identify set.
+        self.S = None   # Number of sets in cache.
+        self.e = None   # Number of bits to identify line.
+        self.E = None   # Number of lines per set.
+        self.b = None   # Number of bits to identify block byte.
+        self.B = None   # Number of bytes in a block.
+        self.m = None   # Total number of identifying bits in address.
+        self.M = None   # Total addresses in cache.
+
+        self.cache = None
+        self.access_log = None
+        self.hits = None
+        self.misses = None
+        self.evictions = None
+        self.verbose = False
+
+    def main(self, s, E, b, file_name, verbose):
+        """
+        Program main.
+        """
+        if verbose:
+            self.verbose = True
+
+        # Calculate total sizes of cache structures.
+        self.s = s
+        self.S = int(math.pow(2, self.s))
+        self.E = E
+        self.e = int(math.log2(self.E))
+        self.b = b
+        self.B = int(math.pow(2, self.b))
+        self.m = self.E * self.S
+        # self.M = int(math.pow(2, self.m))
+        self.M = int(math.pow(2, 64))
+
+        # Create cache object.
+        self.cache = []
+        self.access_log = []
+        for set in range(self.S):
+            self.cache.append([])
+            self.access_log.append([])
+
+            # Create lines in set.
+            for line in range(self.E):
+                self.cache[len(self.cache) - 1].append({
+                    'valid': False,
+                    'tag': 0,
+                    'block': [],
+                })
+
+        # Run simulation on cache, using file.
+        self.run_simulation(file_name)
+
+    def run_simulation(self, file_name):
+        """
+        Runs full simulation of cache, using given input file.
+        :param file_name: Name of file to read from.
+        """
+        self.hits = 0
+        self.misses = 0
+        self.evictions = 0
+
+        # Open file to read in values.
+        with open(file_name) as file:
+            # Read in line.
+            for line in file:
+                if line.strip() != '':
+                    self.handle_line(line)
+
+        print('hits:{0} misses:{1} evictions:{2}'.format(self.hits, self.misses, self.evictions))
+
+    def handle_line(self, line, main_bool=True):
+        """
+        Handles cache simulation for given line instruction.
+        :param line: Line to handle for.
+        :param main_bool: Bool to indicate if this is an initial cache line read in or a subquery from one.
+                Note that only initial cache line read ins will print or update missed/skipped/evicted variables.
+        """
+        split_line = re.split(r'[ ,\n]', line)
+
+        # Check if instruction load. If so, we can skip line.
+        if split_line[0] == 'I':
+            # Found instruction load. Returning.
+            return
+        else:
+            line = line.strip()
+
+            # Not an instruction load. Keep processing. Start by parsing line info.
+            parsed_data = {}
+            try:
+                # Try parsing as hex.
+                address = int(split_line[2], 16)
+            except ValueError:
+                # Invalid address. Attempt to correct bad values.
+                address = ''
+                for character in split_line[2]:
+                    # Check if int value of character is greater than f.
+                    if ord(character.lower()) > 102:
+                        address += 'f'
+                    else:
+                        # Not a value less than "f". Attempt to_lower, I guess?
+                        address += character.lower()
+
+                # Attempt again. On error, assume invalid and skip line.
+                try:
+                    address = int(address, 16)
+                except ValueError:
+                    return
+
+            parsed_data['address'] = address
+
+            # Attempt to read size value. On error, assume invalid and skip line.
+            try:
+                parsed_data['value_size'] = int(split_line[3], 16) - 1
+            except ValueError:
+                return
+
+            # Get mask values.
+            block_mask = self.B - 1
+            block_offset = self.b
+            set_mask = ((self.S << block_offset) - 1) ^ block_mask
+            set_offset = self.s
+            tag_mask = (self.M - 1) ^ set_mask ^ block_mask
+
+            # Use masks to get address chunks.
+            parsed_data['block'] = address & block_mask
+            parsed_data['set'] = (address & set_mask) >> block_offset
+            parsed_data['tag'] = (address & tag_mask) >> (block_offset + set_offset)
+
+            # Check which of 3 operations apply.
+            if split_line[1] == 'L':
+                # Is load.
+                self.process_line(line, parsed_data, main_bool)
+
+            elif split_line[1] == 'S':
+                # Is store.
+                self.process_line(line, parsed_data, main_bool)
+
+            elif split_line[1] == 'M':
+                # Load then store.
+                load_result = self.process_line(line, parsed_data, False)
+                store_result = self.process_line(line, parsed_data, False)
+
+                # Check if this is an instruction directly from file.
+                if main_bool:
+                    # Is instruction from file. Print data.
+                    if load_result == 0 and store_result == 0:
+                        if self.verbose:
+                            print('{0} hit hit        Decimal Format: {1}'.format(line, parsed_data['address']))
+                        self.hits += 2
+                    elif load_result == 1 and store_result == 0:
+                        if self.verbose:
+                            print('{0} miss hit        Decimal Format: {1}'.format(line, parsed_data['address']))
+                        self.misses += 1
+                        self.hits += 1
+                    elif load_result == 2 and store_result == 0:
+                        if self.verbose:
+                            print('{0} miss eviction hit        Decimal Format: {1}'.format(line, parsed_data['address']))
+                        self.misses += 1
+                        self.evictions += 1
+                        self.hits += 1
+
+            else:
+                # Unknown operation.
+                raise ValueError('Unknown operation "{0}"'.format(split_line[1]))
+
+    def print_cache(self):
+        """
+        Prints simulated cache in an easier to read format.
+        """
+        print('\nCache:')
+        for set_index in range(len(self.cache)):
+            print('  Set {0}:'.format(set_index, self.cache[set_index]))
+            for line_index in range(len(self.cache[set_index])):
+                print('    Line {0}: {1}'.format(line_index, self.cache[set_index][line_index]))
+        print('')
+
+    def process_line(self, line, parsed_data, main_bool):
+        """
+        Handle store instruction. Works as described in class video.
+        :param line: Original line read in.
+        :param parsed_data: Data parsed from line.
+        :param main_bool: Bool to indicate if this is an initial cache line read in or a subquery from one.
+                Note that only initial cache line read ins will print or update missed/skipped/evicted variables.
+        :return: 0 on hit | 1 on miss | 2 on eviction.
+        """
+        set = parsed_data['set']
+
+        # Compare against the all entries in the cache for match.
+        for index in range(len(self.cache[set])):
+            # Check if match.
+            if self.cache[set][index]['valid'] is True and self.cache[set][index]['tag'] == parsed_data['tag']:
+                # Match found.
+                self.process_hit(line, parsed_data, index, main_bool)
+                return 0
+            else:
+                # Not a match. Examine why.
+                if self.cache[set][index]['valid'] is False:
+                    # Cache location was not set. Cold miss. We can just set and return.
+                    self.process_miss(line, parsed_data, index, main_bool)
+                    return 1
+
+        # If we made it this far, cache locations were set, but tags did not match. Conflict miss.
+        self.process_eviction(line, parsed_data, main_bool)
+        return 2
+
+    def process_hit(self, line, parsed_data, index, main_bool):
+        """
+        Processes simulated cache for a hit.
+        :param line: Original line read in.
+        :param parsed_data: Data parsed from line.
+        :param index: Current index in set.
+        :param main_bool: Bool to indicate if this is an initial cache line read in or a subquery from one.
+                Note that only initial cache line read ins will print or update missed/skipped/evicted variables.
+        """
+        if main_bool:
+            # Is main cache file line. Update hits/misses/evictions.
+            self.hits += 1
+
+            # Print if verbose flag set.
+            if self.verbose:
+                print('{0} hit            Decimal Format: {1}'.format(line, parsed_data['address']))
+
+        self.update_access_log(parsed_data['set'], index)
+
+    def process_miss(self, line, parsed_data, index, main_bool):
+        """
+        Processes simulated cache for a miss.
+        :param line: Original line read in.
+        :param parsed_data: Data parsed from line.
+        :param index: Current index in set.
+        :param main_bool: Bool to indicate if this is an initial cache line read in or a subquery from one.
+                Note that only initial cache line read ins will print or update missed/skipped/evicted variables.
+        """
+        if main_bool:
+            # Is main cache file line. Update hits/misses/evictions.
+            self.misses += 1
+
+            # Print if verbose flag set.
+            if self.verbose:
+                print('{0} miss            Decimal Format: {1}'.format(line, parsed_data['address']))
+
+        # Pull out relevant data from passed dict.
+        address = parsed_data['address']
+        value_size = parsed_data['value_size']
+        set = parsed_data['set']
+        tag = parsed_data['tag']
+
+        self.cache[set][index]['valid'] = True
+        self.cache[set][index]['tag'] = tag
+
+        # Set block values that are "loaded" into memory.
+        lower_offset = int(address / self.B) * self.B
+        upper_offset = (int(address / self.B) + 1) * self.B
+        value_offset = address + value_size
+        self.cache[set][index]['block'] = []
+        for i in range(self.B):
+            self.cache[set][index]['block'].append(lower_offset + i)
+
+        self.update_access_log(set, index)
+
+        # Check if size sets address outside of current cache block.
+        if value_offset > upper_offset:
+            # Size sets address outside of current cache block. Update additional blocks as needed.
+            # Start by getting our new size.
+            new_size = value_offset - upper_offset
+            if new_size < 0:
+                new_size = 0
+
+            # Pass our new line data.
+            new_line = ' L {0},{1}'.format(upper_offset, new_size)
+            self.handle_line(new_line, main_bool=False)
+
+    def process_eviction(self, line, parsed_data, main_bool):
+        """
+        Processes simulated cache for an eviction.
+        :param line: Original line read in.
+        :param parsed_data: Data parsed from line.
+        :param main_bool: Bool to indicate if this is an initial cache line read in or a subquery from one.
+                Note that only initial cache line read ins will print or update missed/skipped/evicted variables.
+        """
+        if main_bool:
+            # Is main cache file line. Update hits/misses/evictions.
+            self.misses += 1
+            self.evictions += 1
+
+            # Print if verbose flag set.
+            if self.verbose:
+                print('{0} miss eviction        Decimal Format: {1}'.format(line, parsed_data['address']))
+
+        # Pull out relevant data from passed dict.
+        address = parsed_data['address']
+        value_size = parsed_data['value_size']
+        set = parsed_data['set']
+        tag = parsed_data['tag']
+
+        # Get LRU index from set.
+        index = self.access_log[set][0]
+
+        self.cache[set][index]['tag'] = tag
+
+        # Set block values that are "loaded" into memory.
+        lower_offset = int(address / self.B) * self.B
+        upper_offset = (int(address / self.B) + 1) * self.B
+        value_offset = address + value_size
+        self.cache[set][index]['block'] = []
+        for i in range(self.B):
+            self.cache[set][index]['block'].append(lower_offset + i)
+
+        self.update_access_log(set, index)
+
+        # Check if size sets address outside of current cache block.
+        if value_offset >= upper_offset:
+            # Size sets address outside of current cache block. Update additional blocks as needed.
+            # Start by getting our new size.
+            new_size = value_offset - upper_offset
+            if new_size < 0:
+                new_size = 0
+
+            # Pass our new line data.
+            new_line = ' L {0},{1}'.format(upper_offset, new_size)
+            self.handle_line(new_line, main_bool=False)
+
+    def update_access_log(self, set, index):
+        """
+        Updates set's access log according to "Least Recently Used" ordering.
+        Farther from index 0 means more recently used.
+        :param set: Set to update access log of.
+        :param index: Index to update in set.
+        """
+        # Check if index is currently in set's log. If so, remove.
+        if index in self.access_log[set]:
+            self.access_log[set].remove(index)
+
+        # Add index to end of current set's log. That way, log is organized starting with "least used" first.
+        self.access_log[set].append(index)
+
+
+if __name__ == '__main__':
+    # Define our argparser and get command line args.
+    parser = argparse.ArgumentParser(description='Cache Simulator')
+    parser.add_argument('-v', '-V', action='store_true', default=False, help='Flag for verbose mode.', dest='verbose')
+    parser.add_argument('-s', action='store', required=True, help='Total sets.', dest='set_count')
+    parser.add_argument('-E', action='store', required=True, help='Lines per set.', dest='line_count')
+    parser.add_argument('-b', action='store', required=True, help='Bytes per block.', dest='block_size')
+    parser.add_argument('-t', '-T', action='store', required=True, help='File to read in.', dest='file_name')
+    args = parser.parse_args()
+
+    # If args parsed properly, run program main.
+    CacheSimulator().main(int(args.set_count), int(args.line_count), int(args.block_size), args.file_name, args.verbose)
diff --git a/makefile b/makefile
deleted file mode 100644
index bd2568aed1d28e23f307dd7dcf88390348d675ee..0000000000000000000000000000000000000000
--- a/makefile
+++ /dev/null
@@ -1,42 +0,0 @@
-
-# Tell makefile to use commands defined here, if file with same name exists.
-.PHONY: all compile run valgrind clean
-# Set default if none is specified.
-default: valgrind clean
-
-
-# Fully build and execute project.
-all: compile run clean
-
-
-# Compile program.
-compile:
-	gcc -Wall -Wpedantic -std=c99 *.c
-
-
-# Compile and run program.
-run: compile
-	./a.out
-
-
-# Compile program and run with valgrind (memory checking).
-valgrind: compile
-	valgrind --leak-check=full ./a.out
-
-
-# Clean up directory.
-clean:
-	@rm -f *.o
-	@rm -f a.out
-
-
-# Display help output.
-help:
-	@echo "Default: Executes \"valgrind\", then \"clean\"."
-	@echo ""
-	@echo "Available Commands:"
-	@echo "    all         - Executes \"run\", then \"clean\"."
-	@echo "    compile     - Compiles program to \"a.out\"."
-	@echo "    run         - Compiles program to \"a.out\". Then run."
-	@echo "    valgrind    - Compiles program to \"a.out\". Then run with memory error checking."
-	@echo "    clean       - Remove temporary/compiled files from directory."
diff --git a/readme.md b/readme.md
index bf76f33ac138453332093b0903e55e124667cfd7..1b7e97086034a555729778a3eb120a2772564e40 100644
--- a/readme.md
+++ b/readme.md
@@ -1,8 +1,8 @@
-# C - Cache Simulator
+# Python - Cache Simulator
 
 
 ## Language
-C
+Python
 
 
 ## Description
@@ -11,8 +11,8 @@ A project to simulate system use of a cache for data storage.
 
 ## Running Program
 Open a terminal and cd to project root.<br>
-For a list of available makefile commands, run `make help`.<br>
-In a terminal, run `make <command_here>`.
+Use the `run.sh` bash script to execute file.<br>
+For example, type `./run.sh -v -s 2 -E 1 -b 4 -t ./wmucachelab/traces/dave.trace`
 
 
 ## References
diff --git a/run.sh b/run.sh
index a1b3aad1dfa91f1ee3fb9f3392b7138a116d9182..69730dec3a4bd074539cc5e5b849c2f5f16d6033 100755
--- a/run.sh
+++ b/run.sh
@@ -23,12 +23,13 @@ cd "$(dirname "$0")"
 
 
 function main () {
-    # Check if first arg was provided.
-    if [[ $1 != "" ]]
+    # Check that required number of args were provided.
+    if [[ $# -lt 8 ]]
     then
-        execute_single_file $@
+        # Not enough arguments. Display help.
+        display_help
     else
-        execute_all_files $@
+        execute_single_file $@
     fi
 
     echo ""
@@ -57,26 +58,26 @@ function execute_single_file () {
             then
                 file=$return_value
 
-                echo -e "${color_blue}Found input file \"$file\". Attempting to compile C code.${color_reset}"
-
-                # Attempt to compile C code.
-                make compile
+                echo -e "${color_blue}Found input file \"$file\".${color_reset}"
 
                 # Run input file on example cache simulator file.
                 echo ""
                 echo -e "${color_blue}Running input file on \"wmucachelab/csim-ref\". Runtime args:${color_reset}"
-                echo -e "${color_blue}    -v -s 4 -E 1 -b 4 -t $file${color_reset}"
-                ./wmucachelab/csim-ref -v -s 4 -E 1 -b 4 -t $file
+                echo -e "${color_blue}    $@${color_reset}"
+                ./wmucachelab/csim-ref $@
+#                ./wmucachelab/csim-ref -v -s 1 -E 2 -b 1 -t $file
 
                 # Run input file on compiled C code.
                 echo ""
-                echo -e "${color_blue}Running input file on compiled C code. Runtime args:${color_reset}"
-                echo -e "${color_blue}    -v -s 4 -E 1 -b 4 -t $file${color_reset}"
-                valgrind --leak-check=full ./a.out -v -s 4 -E 1 -b 4 -t $file
+                echo -e "${color_blue}Running input file on Python code. Runtime args:${color_reset}"
+                echo -e "${color_blue}    $@${color_reset}"
+#                python3 main.py $@
+                valgrind --leak-check=full ./a.out $@
 
                 # Clean up directory.
                 make clean
 
+
             else
                 echo -e "${color_red}Failed to determine absolute path for input file.${color_reset}"
                 echo "Terminating script."
@@ -93,15 +94,6 @@ function execute_single_file () {
 }
 
 
-###
- # Runs all cache simulation files.
- ##
-function execute_all_files () {
-    echo -e "${color_red}Not yet implemented.${color_reset}"
-    exit 1
-}
-
-
 ###
  # Gets absolute path of provided file/directory.
  ##
@@ -117,4 +109,23 @@ function get_absolute_path () {
 }
 
 
+function display_help () {
+    echo ""
+    echo "Cache Simulator"
+    echo "Description:"
+    echo "   Runs two cache simulator programs."
+    echo "   First one is a premade, \"correct\" simulator with proper results."
+    echo "   The second one is this custom Python implementation."
+    echo "   Run one after another to compare results."
+    echo ""
+    echo "Options:"
+    echo "   -s <#>     Number of set bits for cache."
+    echo "   -E <#>     Number of lines per set for cache."
+    echo "   -b <#>     Number of block offset bits for cache."
+    echo "   -t <file>  Input file to use as trace."
+    echo "   -v         Optional verbose flag."
+    echo ""
+}
+
+
 main $@
diff --git a/wmucachelab/traces/book_pg660_nice.trace b/wmucachelab/traces/book_pg660_nice.trace
new file mode 100644
index 0000000000000000000000000000000000000000..8e5202aac4c2f19b35f1237a3b142a80259d2128
--- /dev/null
+++ b/wmucachelab/traces/book_pg660_nice.trace
@@ -0,0 +1,16 @@
+ S 0,4
+ S 30,4
+ S 4,4
+ S 34,4
+ S 8,4
+ S 38,4
+ S C,4
+ S 3C,4
+ S 10,4
+ S 40,4
+ S 14,4
+ S 44,4
+ S 18,4
+ S 48,4
+ S 1C,4
+ S 4C,4
diff --git a/wmucachelab/traces/book_pg660_thrashing.trace b/wmucachelab/traces/book_pg660_thrashing.trace
new file mode 100644
index 0000000000000000000000000000000000000000..fd2fc91edd973c885a74f69a3cdbfdf5188e4719
--- /dev/null
+++ b/wmucachelab/traces/book_pg660_thrashing.trace
@@ -0,0 +1,16 @@
+ S 0,4
+ S 20,4
+ S 4,4
+ S 24,4
+ S 8,4
+ S 28,4
+ S C,4
+ S 2C,4
+ S 16,4
+ S 30,4
+ S 14,4
+ S 34,4
+ S 18,4
+ S 38,4
+ S 1C,4
+ S 3C,4
diff --git a/wmucachelab/traces/class_video_example.trace b/wmucachelab/traces/class_video_example.trace
new file mode 100644
index 0000000000000000000000000000000000000000..bc0c62d3bef5c3f6ff9a613341f928c1ef4dccb9
--- /dev/null
+++ b/wmucachelab/traces/class_video_example.trace
@@ -0,0 +1,5 @@
+ S 0,1
+ S 1,1
+ S 7,1
+ S 8,1
+ S 0,1